tag:blogger.com,1999:blog-26496884158684126222024-03-14T05:20:46.619+08:00Pou's IT LifeLearning and Implementing!Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.comBlogger59125tag:blogger.com,1999:blog-2649688415868412622.post-12141412827714090372020-08-16T16:38:00.005+08:002020-08-17T10:25:03.626+08:00[Flutter] 實現 iOS 在 UIActivityViewController 加入行事曆<p>如果要在 Flutter 呼叫分享很簡單,利用 <a href="https://pub.dev/packages/share" target="_parent">share</a> plugin 一下就完成了。開發 Flutter 的人一定都知道。</p>
<p>剛好今天有需求是希望在 iOS 分享活動連結時可以多顯示一個自訂的按鈕,讓使用者可以加入到行事曆。</p>
<a name='more'></a>
<p>要做到 iOS 分享時加入自訂的按鈕,有兩個重要的元素:<a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a> 與 <a href="http://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a>。</p>
<h5><a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a></h5>
<p>系統提供多種 services (例如:複製內容到剪貼簿,分享文字到 social media 或 email ...等)。也提供開發者自訂 service 來提供服務。</p>
<p><a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a> 集合這些 services 來呈現在畫面上供用戶選擇。可設定該 View Controller 傳遞的資料結構與對應的 services。</p>
<p>最簡單的分享如下:</p>
<pre>
<code class="language-javascript">let shareItems = ["Hello"]
let activityVC = UIActivityViewController(activityItems: shareItems, applicationActivities: nil)
self.presentViewController(activityVC, animated: true, completion: nil)</code></pre>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller/1622019-init" target="_blank">activityItems</a>:資料物件的 array,可以是字串,圖片或自訂的資料內容。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller/1622019-init" target="_blank">applicationActivities</a>:<a href="https://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a> 的 array,放有自訂 service 的 <a href="http://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a>。</li>
</ul>
<h5><a href="https://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a></h5>
<p>該類別配合 <a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a> 使用,如果想要提供自訂的 service 給用戶使用,需要繼承該類別來實作並處理用戶傳入的資料做互動。</p>
<p>需要 override 幾個地方:</p>
<ul>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620671-activitytype" target="_blank">activityType</a>:代表該 service 的識別。例如:通常使用 <a href="https://developer.apple.com/documentation/foundation/nsbundle/1418023-bundleidentifier" target="_blank">Bundle.main.bundleIdentifier</a> 加一些自訂的值。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620674-activitytitle" target="_blank">activityTitle</a>:顯示在 <a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a> 的名稱,要記得做多國語系的處理。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620658-activityimage" target="_blank">activityImage</a>:顯示在 <a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityController</a> 的圖示。需要處理不同 iOS 版本需要的圖示大小不一樣。如果使用 iOS 內建系統圖示的話,在 iOS13 可以額外設定大小:<a href="https://developer.apple.com/documentation/uikit/uiimage/symbolconfiguration" target="_blank">UIImage<wbr data-v-4ba5af7c="" />.Symbol<wbr data-v-4ba5af7c="" />Configuration</a>。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620656-activitycategory" target="_blank">activityCategory</a>:定義該 service 的類型,<a href="https://developer.apple.com/documentation/uikit/uiactivity/category" target="_blank">UIActivity<wbr data-v-4ba5af7c="" />.Category</a> 提供了 action 與 share,自訂的 service 要給 action。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620677-canperform" target="_blank">canPerform</a>:可以根據傳入的 data 先做檢查,如果可以處理就回傳 true。</li>
<li><a href="https://developer.apple.com/documentation/uikit/uiactivity/1620668-prepare" target="_blank">prepare</a>:在用戶選擇該 service 時會呼叫該 method,該 method 則實際處理資料被如何使用。如果需要而外的 UI 互動,也是在這裡準備好需要的 view controller。</li>
</ul>
<p> </p>
<p>操作行事曆則需要:<a href="http://developer.apple.com/documentation/eventkit/ekeventstore" target="_blank">EKEventStore</a>,<a href="http://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a>。</p>
<h5><a href="https://developer.apple.com/documentation/eventkit/ekeventstore" target="_blank">EKEventStore</a></h5>
<p>管理存取行事曆與提醒權限的元件。需要在初始化後,利用 <a href="https://developer.apple.com/documentation/eventkit/ekeventstore/1507547-requestaccess"><code data-v-7fb764c1="">request<wbr data-v-7fb764c1="" />Access(to:<wbr data-v-7fb764c1="" />completion:)</code></a> 來取得存取權限。</p>
<p>需要在 Info.plist 加入 <a data-v-c567d8a8="" href="https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW16" target="_blank">NSRemindersUsageDescription</a> 與 <a data-v-c567d8a8="" href="https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW15" target="_blank">NSCalendarsUsageDescription</a> 的宣告,才能使用 EKEventStore。</p>
<h5><a href="https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a></h5>
<p>view controller 用來建立,編輯或刪除行事曆的活動。使用該 view congtroller 的 class 需要實作 <a href="https://developer.apple.com/documentation/eventkitui/ekeventeditviewdelegate" target="_blank">EKEventEditViewDelegate</a>。</p>
<p> </p>
<p>介紹完 iOS 怎麼使用分享介面(<a href="http://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityViewController</a>) 與操作行事曆(<a href="https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a>)後,下面串起來從 Flutter 利用 method channel 通知 iOS 顯示分享的介面。</p>
<h5>1. 實作 EventActivity 繼承 <a href="https://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a> 處理傳入的活動資訊,並呼叫行事曆(<a href="https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a>)</h5>
<p>Info.plist 加入存取行事曆的宣告。</p>
<pre>
<code class="language-xml"><plist version="1.0">
<dict>
...
<key>NSCalendarsUsageDescription</key>
<string>to add this event to your calendar</string>
</dict>
</plist></code></pre>
<p><strong>EventActivity.swift</strong></p>
<pre>
<code class="language-javascript">override func prepare(withActivityItems activityItems: [Any]) {
// 利用 EKEventStore 請求操作 Calendar 的權限
let eventStore = EKEventStore()
eventStore.requestAccess(to: .event) { (granted, error) in
if (granted && error == nil) {
// 先關閉 UIActivityViewController 再開啟 EKEventEditViewController
DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) {
// 把 activityItems 帶入的參數包裝成 EKEvent
let event = self.genereateEvent(eventStore: eventStore, arguments: activityItems as NSArray);
if (event == nil) {
return
}
// 利用 EKEventStore 把 EKEvent 加入
self.insertEvent(event: event!, eventStore: eventStore)
}
} else {
// 如果被取消授權要顯示訊息告訴使用者
self.showAccessDeinedOrRestricted()
}
}
}</code></pre>
<p>利用 <a href="https://developer.apple.com/documentation/eventkit/ekeventstore" target="_blank">EKEventStore</a> 請求並取得權限之後,利用 <a href="https://developer.apple.com/documentation/dispatch/dispatchqueue/2300020-asyncafter" target="_blank">DispatchQueue.main.asyncAfter</a>(deadline: .now() + 0.7) 延後 7 秒的方式來開啟 <a href="https://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a>。</p>
<p>為什麼需要延後?</p>
<p><u>因為 rootViewController 開啟了 <a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityViewController</a> ,無法再從 <a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank">UIActivityViewController</a> 開一個 ViewController</u>,<strong>需要先關它後才能再開啟</strong> <a href="http://developer.apple.com/documentation/eventkitui/ekeventeditviewcontroller" target="_blank">EKEventEditViewController</a>。</p>
<p> </p>
<h5>2. iOS 定義 method channel 接受來自 Flutter 的傳入活動資訊</h5>
<p>打開 AppDelegate.swift 加入下面的 code:</p>
<pre>
<code class="language-javascript">override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
guard let controller = window?.rootViewController as? FlutterViewController else {
fatalError("rootViewController is not type FlutterViewController")
}
// 定義要處理的 method channel
let methodChannel = FlutterMethodChannel(name: "sample.poumason.dev/channels", binaryMessenger: controller.binaryMessenger)
methodChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
// 處理 shared 的 method
if (call.method == "shared") {
// 呼叫自訂的 UIActivity
self.showSharedActivityViewController(arguments: call.arguments)
result("OK")
return
}
result(FlutterMethodNotImplemented)
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
</code></pre>
<p>詳細介紹整合 method channel 的說明可以參考:<a href="https://flutter.dev/docs/development/platform-integration/platform-channels" target="_blank">Writing custom platform-specific code</a>。</p>
<h5> </h5>
<h5>3. 在 AppDelegate.swift 實現 showSharedActivityViewController method 來呼叫<a href="https://developer.apple.com/documentation/uikit/uiactivityviewcontroller" target="_blank"> UIActivityViewController</a>,並加入自訂的 <a href="https://developer.apple.com/documentation/uikit/uiactivity" target="_blank">UIActivity</a>。</h5>
<pre>
<code class="language-javascript">// 要實現 EKEventEditViewDelegate 接受關閉 EKEventEditViewController 的事件
@objc class AppDelegate: FlutterAppDelegate, EKEventEditViewDelegate {
func eventEditViewController(_ controller: EKEventEditViewController, didCompleteWith action: EKEventEditViewAction)
{
print(action)
controller.dismiss(animated: true, completion: nil)
}
private func showSharedActivityViewController(arguments: Any?) {
if let args = arguments as? Dictionary<String, Any?> , !args.isEmpty {
guard let url = args["url"] as? String, !url.isEmpty else {
print("no any be shared data")
return
}
// 把傳入的資料裝到一個自訂的 Event 資料結構
let event = Event.init(title: args["title"] as? String,
location: args["location"] as? String,
url: args["url"] as? String,
startDate: args["startDate"] as? Double,
endDate: args["endDate"] as? Double)
let items: [Any]
let activities: [UIActivity]?
// 判斷如果是 Event 類型才呼叫自訂的 EventActivity,不然視為一般的分享
if (event.isValidated()) {
items = [ url, event ]
activities = [ EventActivity() ]
} else {
items = [ url ]
activities = nil
}
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: activities)
self.window.rootViewController?.present(activityVC, animated: true, completion: nil)
}
}
}</code></pre>
<p> </p>
<h5>4. 從 Flutter 使用 method channel 送出資料</h5>
<pre>
<code class="language-java">Future<void> _sharedEvent() async {
// 建立相同 name 的 method channel
final platform = const MethodChannel('sample.poumason.dev/channels');
try {
// 呼叫定義好的 shared method name
var result = await platform.invokeMethod('shared', {
'url': _urlKey.currentState.value,
'title': _titleKey.currentState.value,
'location': _addressKey.currentState.value,
'startDate':
(_startKey.currentState.value.millisecondsSinceEpoch / 1000).roundToDouble(),
'endDate': (_endKey.currentState.value.millisecondsSinceEpoch / 1000).roundToDouble(),
});
print(result);
} on PlatformException catch (e) {
print(e.message);
}
}</code></pre>
<p>method channel 傳遞參數的類型是有限制的,可以參考 <a href="https://flutter.dev/docs/development/platform-integration/platform-channels" target="_blank">Platform channel data types support and codecs</a> 的定義。</p>
<p> </p>
<h5>5. 範例結果</h5>
<table>
<tbody>
<tr>
<td><img src="https://dotblogsfile.blob.core.windows.net/user/pou/6f45281a-45a0-4f3c-9d4b-0b10c0f6cbff/1597566128.png" style="width: 250px; height: 445px;" /></td>
<td><img src="https://dotblogsfile.blob.core.windows.net/user/pou/6f45281a-45a0-4f3c-9d4b-0b10c0f6cbff/1597566139.png" style="width: 250px; height: 445px;" /></td>
<td><img src="https://dotblogsfile.blob.core.windows.net/user/pou/6f45281a-45a0-4f3c-9d4b-0b10c0f6cbff/1597566151.png" style="width: 250px; height: 445px;" /></td>
</tr>
</tbody>
</table>
<h5>範例程式:<a href="https://github.com/poumason/flutter_samples/tree/master/share_calendar" target="_blank">share_calednar</a>。</h5>
<p>===</p>
<p>以上是介紹如何從 Flutter 加入活動資料到 iOS 的行事曆。希望對大家有所幫助,謝謝。</p>
<p> </p>
<h6>參考資料</h6>
<ul>
<li><a href="https://www.itread01.com/content/1545857656.html" target="_blank">[iOS] Get the name of the running application</a></li>
<li><a href="https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundledisplayname" target="_blank">CFBundleDisplayName </a></li>
<li><a href="https://stackoverflow.max-everyday.com/2018/01/ios-swift-use-of-s-in-stringformat/" target="_blank">[iOS] swift : use of %s in String(format: …)</a></li>
<li><a href="https://stackoverflow.com/questions/51593790/remove-html-tags-from-a-string-in-dart" target="_blank">Remove HTML tags from a String in Dart</a></li>
<li><a href="https://medium.com/@fede_nieto/manage-calendar-events-with-eventkit-and-eventkitui-with-swift-74e1ecbe2524" target="_blank">Manage calendar events with EventKit and EventKitUI with Swift</a></li>
<li><a href="https://medium.com/@zhongwei0717/ios-%E7%9A%84%E5%8E%9F%E7%94%9F%E5%88%86%E4%BA%AB-uiactivityviewcontroller-f73d272dc2a3" target="_blank">iOS 的原生分享, UIActivityViewController</a></li>
<li><a href="https://dev.to/nemecek_f/how-to-use-ekeventeditviewcontroller-in-swift-to-let-user-save-event-to-ios-calendar-d8" target="_blank">How to use EKEventEditViewController in Swift to let user save event to iOS calendar</a></li>
<li><a href="https://medium.com/@JJeremy.XUE/swift-%E7%8E%A9%E7%8E%A9-uiactivityviewcontroller-5995bb80ff68" target="_blank">Swift — 玩玩 UIActivityViewController</a></li>
<li><a href="https://www.simpleswiftguide.com/how-to-use-sf-symbols-in-swiftui/" target="_blank">How to use SF Symbols in SwiftUI</a></li>
<li><a href="https://www.hackingwithswift.com/articles/118/uiactivityviewcontroller-by-example" target="_blank">UIActivityViewController by example</a></li>
<li><a href="https://ithelp.ithome.com.tw/articles/10223393" target="_blank">【Flutter基礎概念與實作】 Day18–Flutter測試框架以及Mockito Package使用範例介紹</a></li>
<li><a href="https://medium.com/@craiggrummitt/sf-symbols-in-ios-13-55e5febf6db6" target="_blank">SF Symbols in iOS 13</a></li>
<li><a href="https://www.appcoda.com.tw/social-framework-introduction/" target="_blank">初學者指南:使用社交框架與 UIActivityViewController</a></li>
<li><a href="https://medium.com/flutterpub/sample-form-part-1-flutter-35664d57b0e5" target="_blank">Sample Form — Part 1— Flutter</a></li>
<li><a href="https://pub.dev/packages/datetime_picker_formfield" target="_blank">datetime_picker_formfield</a></li>
</ul>
Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-13843393592217912562020-06-20T17:52:00.002+08:002020-06-22T12:09:02.712+08:00程式化發送 GitHub 的 PullRequest,解決手動 winget 上架人都是想偷懶的,本篇介紹利用程式化發 Pull Reqeust,解決手動上新的版本到 winget 的流程。
<a name='more'></a>
<p>微軟在 <a href="https://www.zdnet.com/article/microsofts-windows-package-manager-this-command-line-tool-can-install-all-your-apps/" target="_blank">//build 2020</a> 推出了 <a href="https://docs.microsoft.com/zh-tw/windows/package-manager/winget/">winget</a> 可讓開發人員在 Windows 10 電腦上探索、安裝、升級、移除和設定應用程式。</p>
<p>讓我想要把自己的程式也放到 <a href="https://docs.microsoft.com/zh-tw/windows/package-manager/winget/">winget</a> 上,讓其他人也可以直接下載。</p>
<p>要怎麼送程式到 winget 呢?</p>
<p>根據 <a href="https://docs.microsoft.com/en-us/windows/package-manager/package/" target="_blank">Submit packages to Windows Packages Manager</a> 介紹,目前 <a href="http://docs.microsoft.com/zh-tw/windows/package-manager/winget/" target="_blank">winget</a> 沒有上架的工具,要上程式到 winget 有兩個步驟:</p>
<ol>
<li>建立一份 manifest,提供程式的說明(作者, 授權, 下載 URL, checksum, ... 等)</li>
<li>藉由 GitHub 來發 Pull Reqeust 到 <a href="https://github.com/microsoft/winget-pkgs" target="_blank">winget-pkgs</a> repo</li>
</ol>
<h2><a href="http://docs.microsoft.com/zh-tw/windows/package-manager/winget/" target="_blank">Create your package manifest</a></h2>
<p>manifest 是一份 YAML 描述程式的基本資訊,基本必要資訊如下:</p>
<pre><code class="language-css">Id: string # Publisher.package format. (必須是唯一的)
Publisher: string # The name of the publisher.
Name: string # The name of the application.
Version: string # Version numbering format.
License: string # The open source license or copyright.
InstallerType: string # Enumeration of supported installer types (exe, msi, msix, inno, wix, nullsoft, appx).
Installers:
- Arch: string # Enumeration of supported architectures.
- Url: string # Path to download installation file.
- Sha256: string # SHA256 calculated from installer.
ManifestVersion: 0.1.0</code></pre>
<p>範例如:</p>
<pre><code class="language-css">Id: microsoft.teams
Publisher: Microsoft Corporation
Name: Microsoft Teams
Version: 1.3.0.4461
License: Copyright (c) Microsoft Corporation. All rights reserved.
InstallerType: exe
Installers:
- Arch: x64
Url: https://statics.teams.cdn.office.net/production-windows-x64/1.3.00.4461/Teams_windows_x64.exe
Sha256: 712f139d71e56bfb306e4a7b739b0e1109abb662dfa164192a5cfd6adb24a4e1
ManifestVersion: 0.1.0</code></pre>
<p>介紹幾個比較特別的參數:</p>
<ul>
<li>Silent:可以強迫在安裝過程沒有出現任何需要人為操作的畫面,在 winget 安裝非常適合</li>
<li>SilentWithProgress:功能類似 Silent</li>
<li>Commands:可搭配在安裝時執行特定的指令</li>
</ul>
<p>更多詳細的 schema 可以參考:<a href="https://docs.microsoft.com/en-us/windows/package-manager/package/manifest?tabs=minexample%2Ccompschema#complete-schema" target="_blank">Complete Shcema</a>。</p>
<p>了解 manifest 的格式之後,下面介紹怎麼建立 manifest:</p>
<ol>
<li>登入 GitHub 帳號</li>
<li>fork <a href="https://github.com/microsoft/winget-pkgs" target="_blank">winget-pkgs</a> repo 到自己的帳號下</li>
<li>clone 自己帳號下的 winget-pkgs repo 到本機</li>
<li>進入 clone 下來的 manifest 目錄,建立專屬的 {publisher} 目錄,例如:我是 pou lin。如果未來同一個 {publisher} 發多個 apps <u><strong>都只能</strong></u>放在這個 {publisher} 目錄</li>
<li>在 {publisher} 目錄下,建立 {app} 目錄,例如:我的程式叫 just love radio,目錄名稱就是 just love radio。</li>
<li>在 {app} 目錄下,建立 {version}.yaml。{version}.yaml 每個不同版本都會有一份,例如:1.0.0.0.yaml,1.0.2.0.yaml
<ul>
<li>產生 yaml 檔案,可以 Tool 目錄中找到 <a href="https://github.com/microsoft/winget-pkgs/tree/master/Tools" target="_blank">YamlCreate.ps1</a>,利用 Powershell 執行它來加以建立 yaml</li>
<li>目錄結構如圖範例:<img src="https://dotblogsfile.blob.core.windows.net/user/pou/3f9e2784-c961-421c-8bc0-9ba99428b179/1592638339.png" style="height: 117px; width: 300px;" /></li>
</ul>
</li>
<li>準備好 {version}.yaml 可以發 git push 到自己的 repo</li>
<li>最後在自己的 winget-pkgs repo 發一個 PullRequest 到 microsoft/winget-pkgs 就完成了</li>
</ol>
<p>建立 manifest 需要注意的地方,可以參考:<a href="https://docs.microsoft.com/en-us/windows/package-manager/package/manifest?tabs=minexample%2Ccompschema#tips-and-best-practices" target="_blank">Tips and best practices</a>。</p>
<p> </p>
<h2>Custom Powershell script to auto create YAML and send Pull Request to GitHub</h2>
<p>上面介紹手動處理,雖然步驟不多,但每次有新版本發布都要再做一次。我很懶惰,身為開發人員能減少這種手動的流程是最基本的。</p>
<p>分析 {manifest}.yaml 的基本參數跟學習 GitHub 的 <a href="https://developer.github.com/v4/" target="_blank">GraphQL API </a>後,我寫出了 Powershell 的 script 來自動化幫我完成手動的流程。</p>
<p>要自動化處理這些流程有幾個重要的組成元素:</p>
<h3>1. 準備可以檢查是否有新版的地方</h3>
<p>不管是用 Jenkins 或其他 CI,建議準備一個地方紀錄目前最新版本的資訊,讓 script 可以檢查是否有新的版本要發給 winget。</p>
<p>例如:我準備一個 json 檔案,再每次 Jenkins 建立新版本時,自動更新該份 json 留下最新的版本資訊。</p><p>內文如下:</p>
<pre><code class="language-json">{
"packages": [
{
"version" : "10.0.2",
"code" : 100002,
"url" : "https://poumason.internal.com/jlr/100002.msi"
},
...
]
}</code></pre>
<h3>2. 自動檢查更新並產生 {version}.yaml 的內容</h3>
<p>根據 <a href="https://docs.microsoft.com/en-us/windows/package-manager/package/manifest?tabs=minexample%2Ccompschema#minimal-required-schema" target="_blank">Minimal required schema</a> 定義,準備需要的參數,如下:</p>
<pre><code class="powershell">Write-Host "1. 從 https://poumason.internal.com/jlr/version.json 檢查是否有新版本"
$response = Invoke-WebRequest -Uri "https://poumason.internal.com/jlr/version.json"
$jsonObj = ConvertFrom-Json $([String]::new($response.Content))
$lastestVer = $jsonObj.versions[0].version
$lastestUrl = $jsonObj.supports[0].url
$previousVer = $lastestVer
# 讀取本機的暫存檔,檢查上次抓到的版本跟最新的版本是否一致
$cacheFile = "..\.\winget_ver.txt"
if(![System.IO.File]::Exists($cacheFile)){ SET-Content -Path $cacheFile -Value $lastestVer } else { $previousVer = Get-Content -Path $cacheFile }
if ($previousVer -eq $lastestVer) {
Write-Host "*** The same version ***" -ForeGroundColor Blue
} else {
# 準備一個新的 branch
$branchName = "jlr-${lastestVer}"
git branch -D $branchName
git branch $branchName
git checkout $branchName
# 準備一個存安裝檔
$exeFile = ".\jlr-${lastestVer}.msi";
Write-Host "2. 下載 url 中的檔案 ${exeFile}"
Invoke-WebRequest $lastestUrl -OutFile $exeFile
Write-Host "3. 取得下載好檔案的 checksum ${exeFile}"
$checkSum = (Get-FileHash $exeFile -Algorithm sha256).hash
rm $exeFile
Write-Host "4. 產生新版本的 ${lastestVer}.yaml"
# publisherFolder 可根據自己的名稱與 App 名稱做改變
$publisherFolder = "poulin\jlr"
$fileName = ".\manifests\${publisherFolder}\${lastestVer}.yaml"
# 寫入 ID
$string = "Id: poulin.jlr"
write-output $string | out-file $filename
# 寫入 Version
$string = "Version: " + $lastestVer
write-output $string | out-file $filename -append
# 寫入 App 名稱
$string = "Name: Just Love Radio"
write-output $string | out-file $filename -append
# 寫入 Publisher
$string = "Publisher: Pou Lin"
write-output $string | out-file $filename -append
# 寫入 License
$string = "License: Copyright (c) Pou Lin All Rights Reserved."
write-output $string | out-file $filename -append
# 寫入 InstallerType
$string = "InstallerType: msi"
write-output $string | out-file $filename -append
$string = "Installers:"
write-output $string | out-file $filename -append
$string = " - Arch: x86"
write-output $string | out-file $filename -append
$string = " Url: " + $lastestUrl
write-output $string | out-file $filename -append
$string = " Sha256: " + $checkSum
write-output $string | out-file $filename -append
# 加入 Silent 與 SilentWithProgress
$string = " Switches:"
write-output $string | out-file $filename -append
$string = " Silent: /S"
write-output $string | out-file $filename -append
$string = " SilentWithProgress: /S"
write-output $string | out-file $filename -append
Write-Host "5. 更新暫存檔案到最新檢查的版本 ${cacheFile}"
SET-Content -Path $cacheFile -Value $lastestVer
Write-host GET-Content -Path $cacheFile
Write-Host "6. 寫入 git commit 並送到自己的 winget-pkgs repo"
git add $fileName
$comment = "Add JLR new version ${lastestVer}"
git commit -m $comment
git push --set-upstream origin $branchName
git push
} </code></pre>
<p><strong><span style="background-color: yellow;">這個 script 需要被放在自己 GitHub 帳號下 clone 到本機的 winget-pkgs 根目錄</span></strong>中,如下圖:<img alt="" height="197" src="data:image/png;base64,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" width="678" /></p>
<p>因為裡面會產生新的 {version}.yaml,它需要被 push 到自己的 repo 中,才能發 pull request 給 microsoft:winget-pkgs repo。</p>
<h3>3. 利用 <a href="https://developer.github.com/v4/" target="_blank">GraphQL API</a> 從自己的 winget-pkgs repo 發 Pull Request 給 microsof:winget-pkgs repo</h3>
<p>要程式化發送 Pull Request 需要使用 <a href="https://developer.github.com/v4/mutation/createpullrequest/" target="_blank">createPullRequest - GraphQL API v4</a>,根據 API 參數說明,我們需要:</p>
<ul>
<li><a href="https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line" target="_blank">Github Personal access token</a>
<ul>
<li>點擊連結申請 Access Token</li>
</ul>
</li>
<li>microsoft:winget-pkgs repo 的 repo ID
<ul>
<li>利用 <a href="https://developer.github.com/v4/explorer/" target="_blank">GraphQL API Explorer</a> 輸入下面的指令得到 repo ID
<pre><code class="language-json">{
repository(owner: "microsoft", name: "winget-pkgs") {
id
}
}</code></pre>
</li>
</ul>
</li>
</ul>
<p>最後準備如下的程式碼:</p>
<pre><code class="powershell">Write-Host "7. create pull request for GitHub"
$graphql = "https://api.github.com/graphql"
$accessToken = "{申請自己專用的 GitHub Access Token}"
# microsoft:winget-pkgs 的 repo id
$repoId = "{}"
# 準備發送 api 的 http request headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("User-Agent", 'Agent')
$headers.Add("Authorization", 'bearer ' + $accessToken)
# 把 $branchName 加入來源
$branchName = "{你的 github 帳號}:${branchName}"
$body = '{ "query": "mutation { createPullRequest(input: { baseRefName: \"master\", headRefName: \"' + $branchName + '\", repositoryId: \"' + $repoId + '\", title: \"' + $comment + '\" , body: \"\"}) { clientMutationId} }" }'
echo $body
$response = Invoke-WebRequest -Uri $graphql -Headers $headers -Method 'POST' -Body $body
echo $response
git checkout master</code></pre>
<p>執行的效果如同 <a href="https://github.com/microsoft/winget-pkgs/pull/1940">https://github.com/microsoft/winget-pkgs/pull/1940</a> 我發送的結果。</p>
<p>重新整理整份的 script 如下:</p>
<pre><code class="powershell">Write-Host "1. 從 https://poumason.internal.com/jlr/version.json 檢查是否有新版本"
$response = Invoke-WebRequest -Uri "https://poumason.internal.com/jlr/version.json"
$jsonObj = ConvertFrom-Json $([String]::new($response.Content))
$lastestVer = $jsonObj.versions[0].version
$lastestUrl = $jsonObj.supports[0].url
$previousVer = $lastestVer
# 讀取本機的暫存檔,檢查上次抓到的版本跟最新的版本是否一致
$cacheFile = "..\.\winget_ver.txt"
if(![System.IO.File]::Exists($cacheFile)){ SET-Content -Path $cacheFile -Value $lastestVer } else { $previousVer = Get-Content -Path $cacheFile }
if ($previousVer -eq $lastestVer) {
Write-Host "*** The same version ***" -ForeGroundColor Blue
} else {
# 準備一個新的 branch
$branchName = "jlr-${lastestVer}"
git branch -D $branchName
git branch $branchName
git checkout $branchName
# 準備一個存安裝檔
$exeFile = ".\jlr-${lastestVer}.msi";
Write-Host "2. 下載 url 中的檔案 ${exeFile}"
Invoke-WebRequest $lastestUrl -OutFile $exeFile
Write-Host "3. 取得下載好檔案的 checksum ${exeFile}"
$checkSum = (Get-FileHash $exeFile -Algorithm sha256).hash
rm $exeFile
Write-Host "4. 產生新版本的 ${lastestVer}.yaml"
# publisherFolder 可根據自己的名稱與 App 名稱做改變
$publisherFolder = "poulin\jlr"
$fileName = ".\manifests\${publisherFolder}\${lastestVer}.yaml"
# 寫入 ID
$string = "Id: poulin.jlr"
write-output $string | out-file $filename
# 寫入 Version
$string = "Version: " + $lastestVer
write-output $string | out-file $filename -append
# 寫入 App 名稱
$string = "Name: Just Love Radio"
write-output $string | out-file $filename -append
# 寫入 Publisher
$string = "Publisher: Pou Lin"
write-output $string | out-file $filename -append
# 寫入 License
$string = "License: Copyright (c) Pou Lin All Rights Reserved."
write-output $string | out-file $filename -append
# 寫入 InstallerType
$string = "InstallerType: msi"
write-output $string | out-file $filename -append
$string = "Installers:"
write-output $string | out-file $filename -append
$string = " - Arch: x86"
write-output $string | out-file $filename -append
$string = " Url: " + $lastestUrl
write-output $string | out-file $filename -append
$string = " Sha256: " + $checkSum
write-output $string | out-file $filename -append
# 加入 Silent 與 SilentWithProgress
$string = " Switches:"
write-output $string | out-file $filename -append
$string = " Silent: /S"
write-output $string | out-file $filename -append
$string = " SilentWithProgress: /S"
write-output $string | out-file $filename -append
Write-Host "5. 更新暫存檔案到最新檢查的版本 ${cacheFile}"
SET-Content -Path $cacheFile -Value $lastestVer
Write-host GET-Content -Path $cacheFile
Write-Host "6. 寫入 git commit 並送到自己的 winget-pkgs repo"
git add $fileName
$comment = "Add JLR new version ${lastestVer}"
git commit -m $comment
git push --set-upstream origin $branchName
git push
Write-Host "7. create pull request for GitHub"
$graphql = "https://api.github.com/graphql"
$accessToken = "{申請自己專用的 GitHub Access Token}"
# microsoft:winget-pkgs 的 repo id
$repoId = "{}"
# 準備發送 api 的 http request headers
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Add("User-Agent", 'Agent')
$headers.Add("Authorization", 'bearer ' + $accessToken)
# 把 $branchName 加入來源
$branchName = "{你的 github 帳號}:${branchName}"
$body = '{ "query": "mutation { createPullRequest(input: { baseRefName: \"master\", headRefName: \"' + $branchName + '\", repositoryId: \"' + $repoId + '\", title: \"' + $comment + '\" , body: \"\"}) { clientMutationId} }" }'
echo $body
$response = Invoke-WebRequest -Uri $graphql -Headers $headers -Method 'POST' -Body $body
echo $response
git checkout master
} </code></pre>
<p>再次強調這個 script 要放在跟自己 winget-pkgs 的目錄下,這樣才能正常執行。</p>
<p>===</p>
<p>在研究 <a href="https://developer.github.com/v4/" target="_blank">GraphQL API</a> 跟 Powershell 指令花了不少時間,感謝 <a href="https://github.com/joewen" target="_blank">Joe Wen</a> 的幫忙讓我加快不少。</p>
<p>上面的 script 歡迎大家使用,如果有任何的 script 的問題歡迎跟我說,希望有幫忙到大家,謝謝。</p>
<p><strong>參考資料</strong></p>
<ul>
<li><a href="https://developer.github.com/v4/mutation/createpullrequest/" target="_blank">createPullRequest - GraphQL API v4</a></li>
<li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-webrequest?view=powershell-7" target="_blank">Invoke-WebRequest</a></li>
<li><a href="https://davidhamann.de/2019/04/12/powershell-invoke-webrequest-by-example/" target="_blank">HTTP requests with PowerShell’s Invoke-WebRequest – by Example</a></li>
<li><a href="https://jonlabelle.com/snippets/view/powershell/send-a-json-http-api-request-in-powershell" target="_blank">Send a JSON HTTP API Request in PowerShell</a></li>
<li><a href="https://www.systemcenterautomation.com/2019/05/building-json-payload-in-powershell/" target="_blank">Building JSON Payload in Powershell</a></li>
<li><a href="https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line" target="_blank">Creating a personal access token for the command line</a></li>
<li><a href="https://stackoverflow.com/questions/12936150/is-it-possible-to-send-additional-http-headers-to-web-services-via-new-webservic" target="_blank">Is it possible to send additional HTTP headers to web services via New-WebServiceProxy</a></li>
</ul>
Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-12835481978560445092019-10-20T11:58:00.003+08:002019-10-21T15:44:24.981+08:00教學如何手動把 WPF 封裝成 appx隨著微軟提供 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-root" target="_blank">Desktop Bridge</a> 的技術,讓原本開發 WPF 或 Win32 的開發人員方便包裝既有的程式來上架到 Store ,減少重新開發 UWP 的成本。本篇將説明如何手動的方式把既有的 WPF 封裝到 appx 之中。<br />
<a name='more'></a><br />
把既有的 WPF 或 Win32 程式封裝成 *.appx 放到 Store 有什麽好處?<br />
<ol><li>讓既有程式可以操作 Windows Universal Platform (UWP) APIs;可參考 <a href="https://dotblogs.com.tw/pou/2018/12/31/184154" target="_blank">WPF 使用 Windows 10 APIs - 1</a>, <a href="https://dotblogs.com.tw/pou/2019/02/26/103056" target="_blank">2</a> 與 <a href="https://dotblogs.com.tw/pou/2019/04/12/000903" target="_blank">3</a> 的介紹;</li>
<li>方便的部署,搭配 Store 收集用戶的 review, 下載數, crashes 等;</li>
<li>... 還有很多這裏就不多做說明;</li>
</ol>既有的 WPF 封裝到 *.appx 之前,微軟建議先閲讀 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-prepare" target="_blank">Prepare to package your desktop app</a> 瞭解封裝之後有那些功能不支援或是 .NET Framework 最低需求與各個版本對應的差別,... 等,但微軟不强迫調整,只是開發人員需要自己做相容測試,例如 COM 的使用有很多不支援或是更換整合的方式來繼續支援。<br />
<br />
在 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-root" target="_blank">Package desktop applications (Desktop Bridge)</a> 提供多種方式:<br />
<ul><li>Build an MSIX from an existing app installer<div>利用 <a href="https://docs.microsoft.com/en-us/windows/msix/mpt-overview" target="_blank">MSIX Packaging Tool</a> 或 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-run-desktop-app-converter" target="_blank">Desktop App Converter</a> 要注意兩者工具<u>支援封裝與被安裝時要求的 Windows 10 版本有所不同</u>。</div></li>
<li>Build an MSIX from source code using Visual Studio<div>在既有的方案加入 <b>Windows Application Package Project</b> 並把 desktop project 加入其中就可以進行封裝與除錯。<br />
這是最簡單的方式,但如果 desktop project 有特殊的流程,例如: 建置出來的 *.dll 要混肴或是 sign 憑證,建議在 desktop project 加入 <b>PostBuildEvent</b> 做處理並輸出到 Windows Application Package Project 中。<br />
不然會遇到 <a href="https://stackoverflow.com/questions/52039591/how-to-package-a-wpf-app-with-post-build-dependencies-into-uwp-package" target="_blank">How to package a WPF app with post build dependencies into UWP package?</a> 的問題。</div></li>
<li>Third-party installers<div>可參考 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-third-party-installer" target="_blank">Package a desktop application using third-party installers</a> 介紹的第三方工具來執行,相對的有些要收費。</div></li>
<li>Manual packaging<div>利用 <b>MakeAppx.exe</b> 來封裝 Windows app package。可參考 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion" target="_blank">Package a desktop application manually</a>。</div></li>
</ul><br />
利用 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion" target="_blank">Package a desktop application manually</a> 介紹怎麽手動封裝成 *.appx;<br />
<ol><li>建立 <b><a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion#create-a-package-manifest" target="_blank">appxmanifest.xml</a></b><div><pre class="code prettyprint"><code class="xml"><?xml version="1.0" encoding="utf-8" ?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<!--
Identify 的參數可到 Partner Center 中產品的 Product identify 取得;
ProcessorArchitecture 根據您預設支援的平臺設定;
-->
<Identity Name="{Package/Identity/Name}"
Publisher="{Package/Identity/Publisher}"
Version="9.9.9.9"
ProcessorArchitecture="x86" />
<Properties>
<DisplayName>WPFWindows</DisplayName>
<PublisherDisplayName>{Package/Properties/PublisherDisplayName}</PublisherDisplayName>
<Logo>AppIcon\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<!-- 要設定 Windows.Desktop 因爲其他設備不支援 -->
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.16299.0" MaxVersionTested="10.0.18362.0" />
</Dependencies>
<Resources>
<Resource Language="en"/>
<Resource Language="ja"/>
<Resource Language="zh-Hans"/>
<Resource Language="zh-Hant"/>
</Resources>
<Applications>
<Application Id="App" Executable="WPFWindows.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="WPFWindows"
Description="WPFWindows"
BackgroundColor="#00AED8"
Square150x150Logo="AppIcon\Square150x150Logo.png"
Square44x44Logo="AppIcon\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="AppIcon\Wide310x150Logo.png" />
<uap:SplashScreen Image="AppIcon\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package></code></pre>需要注意的地方:<br />
<ul><li>根據您在 <a href="https://partner.microsoft.com/" target="_blank">Partner Center</a> 注冊的 product 填入它所屬 product identify 中必要的參數:<div><ul><li>Package/Identity/Name</li>
<li>Package/Identity/Publisher</li>
<li>Package/Properties/PublisherDisplayName</li>
</ul>如果不知道怎麽填寫,可以參考 <a href="https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/generate-package-manifest" target="_blank">How Visual Studio generates an app package manifest</a> 裏面的説明;</div></li>
<li><Resources /> 記得填入 app 支援的多個語言包或是多個 scale(100,125,150,200,400) 的 app icon;</li>
<li><Application /> 的 <span class="inline-code">EntryPoint="Windows.FullTrustApplication"</span> 是固定的;<span class="inline-code">Executable="WPFWindows.exe"</span> 則設定 WPF 的執行檔;如果您使用 <b>Windows Application Package Project</b> 方式,它在 obj/release/packagelayout 會要產生一樣的内容;</li>
<li><Capabilities/> 記得宣告 <span class="inline-code"><rescap:Capability Name="runFullTrust" /></span>;</li>
</ul></div></li>
<li>準備 appx 需要的 Store logo<div>因爲 appxmanifest.xml 從的 tag 寫了這些圖示的 icon,每一張圖要分別準備 scale-100, scale-125, scale-150, scale-200 與 scale-400 五種尺寸。例如:把產生的 app icons 放在一個目錄 AppIcon。<br />
詳細可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/style/app-icons-and-logos" target="_blank">App icons and logos</a> 的介紹或是搭配 <a href="https://blogs.windows.com/windowsdeveloper/2016/02/15/uwp-tile-generator-extension-for-visual-studio/" target="_blank">UWP Tile Generator Extension for Visual Studio</a> 產生圖示;</div></li>
<li>利用 <a href="https://docs.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options" target="_blank">MakePri.exe</a> 建立必要的 priconfig.xml 與多個 resources.pri<div>參考 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion#generate-a-package-resource-index-pri-file" target="_blank">Generate a Package Resource Index (PRI) file</a> 介紹,App 要支援多語系跟多種 scale 圖示時需要將它們分別建立出來,所以先建立 priconfig.xml 定義需要的内容,才能建立相對應的 resources.dll。<br />
步驟:<br />
<ol type="a"><li>假設 WPF 專案的位置在 c:\sources\WPFWindows\bin,那產生的 priconfig.xml 要放在 *.exe 的相同目錄;</li>
<li>把上一步產生的 AppIcon 目錄放到跟 *.exe 相同的目錄;</li>
<li>執行指令 <span class="inline-code">makepri.exe createconfig /pr $srcPath /cf $srcPath\priconfig.xml /dq en-US /o</span><br />
makepri.exe 位置在 <span class="inline">C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64</span> 其中 10.0.18362.0 根據您安裝的 Windows 10 SDK 來更換,後面介紹的 makeappx.exe 與 signtool.exe 也在相同的目錄裏面。<br />
$srcPath 換成專案 *.exe 的路徑。</li>
<li>執行指令 <span class="inline-code">makepri.exe new /pr $srcPath /cf $srcPath\priconfig.xml /o</span><div>如果發現 resources.dll 等多個檔案(根據支援的語系跟 scales) 不在 *.exe 同一個目錄,要記得複製進去。或者,在執行指令前先把 terminal 指定到 *.exe 相同的路徑再執行指令;<br />
原因 makepri.exe new 指令中只能有一個輸出檔案 <span class="inline-code">/of C:\MyApp\src\resources.pri</span>,但是因爲我們需要多個 resources.dll 才會需要先進入該路徑;</div></li>
</ol></div></li>
<li>利用 <a href="https://docs.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool" target="_blank">MakeAppx.exe</a> 封裝既有 desktop project 編譯出來的 *.exe 或 *.dll 變成 *.appx<br />
<div>執行指令 <span class="inline-code">MakeAppx.exe pack /v /h sha256 /d $srcPath /p $appxFile</span>。<br />
$appxFile 換成輸出的目錄,例如:c:\appx\WPFWindows.appx;<br />
這樣就完成了封裝的任務,但是它還無法被人點擊 *.appx 直接安裝,因爲少了憑證的認證。</div></li>
<li>利用 <a href="https://docs.microsoft.com/en-us/dotnet/framework/tools/signtool-exe" target="_blank">SignTool.exe</a> 搭配 *.pfx 或是 *.cer 為 *.appx 加入憑證<div>如果您沒有 Store 專用的 pfx, 可以參考 <a href="https://docs.microsoft.com/en-us/windows/msix/package/create-certificate-package-signing" target="_blank">Create a certificate for package signing</a> 的介紹,建立一個憑證。該憑證需要 identify 等資料(同寫在 appxmanifest.xml 裏的内容),我建議您可以產生一個時間比較長效的,避免每一年需要產生一次。<br />
產生好 *.pfx 之後,把檔案放到 *.exe 來方便執行 <span class="inline-code">SignTool.exe sign /fd SHA256 /a /f $pfxFile $appxFile</span>。<br />
$pfxFile 換成您建立的 *.pfx 檔案。這樣才能點擊 *.appx 來安裝;<br />
[<b>注意</b>]<br />
由於憑證是自己建立的,如果您直接把 *.appx 交給其他電腦安裝會遇到憑證不受信任的錯誤而無法安裝,可以參考下面的指令建立成 *.bat,並把它與 *.appx 與 *.pfx 同一個目錄,點擊它來完整第一次的憑證安裝:<br />
<pre class="code prettyprint"><code>@echo off
pushd "%CD%"
if not "%1"=="am_admin" (powershell start -verb runas '%0' am_admin & exit /b)
CD /D "%~dp0"
echo %cd%
echo "install certification ..."
// myApp_StoreKey.pfx 要換成您建立的憑證名稱
Powershell.exe Import-PfxCertificate -FilePath myApp_StoreKey.pfx -CertStoreLocation Cert:\LocalMachine\Root
echo "install msix ..."
Powershell.exe Add-AppxPackage -path .\*.appx
pause</code></pre></div></li>
<li>如果要包裝成 *.appxbundle:<div>完成 *.appx 的 sign 之後,再利用 MakeAppx.exe 換成封裝成 *.appxbundle 最後再對 *.appxbundle 做 sign。<br />
步驟:<br />
<ol type="a"><li>建立一個目錄 package,放在 *.exe 目錄的上一層或是任何地方;</li>
<li>修改建立 *.appx 時的輸出目錄到 package 目錄</li>
<li>讓 terminal 到 package 目錄再執行 <span class="inline-code">MakeAppx.exe bundle /bv $appxVersion /d $bundlePath /p $appxBundleFile</span>。$appxVersion 換成需要的版本號,$bundlePath 則是把 *.appxbundle 放到 package 目錄,更多 bundle 説明可參考 <a href="https://docs.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool" target="_blank">Create an app bundle</a>。</li>
<li>再 sign 一次:<span class="inline-code">SignTool.exe sign /fd SHA256 /a /f $pfxFile $appxBundleFile</span></li>
</ol></div></li>
</ol><br />
完成上面的步驟就大功告成了。<br />
您可以直接上傳 *.appxbundle 到 partner 或是交給 <a href="https://partner.microsoft.com/dashboard" target="_blank">Partner Center</a> 來上架。<br />
<a name="example-script"></a><br />
完整版的<a href="#example-script">指令</a>如下(以 powershell 來做範例):<br />
<pre class="code prettyprint"><code>$projectFolder = 'WPFWindows'
$srcFolder = '$projectFolder\src'
$setupBundleFolder = '$projectFolder \package'
$appxVersion = '10.0.0.0'
# 建立 appx 與 appxbundle
Write-Host '1. copy appxManifest.xml and AppIcon into srcFolder'
Copy-Item -Path $projectFolder\AppIcon -Destination $srcFolder -force -recurse
Copy-Item -Path $projectFolder\appxManifest.xml -Destination $srcFolder -force -recurse
Write-Host '2. create mutiple resources, msix file and sign it'
$sdkPath = "C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64";
$makePri = "${sdkPath}\makepri.exe"
$makeAppx = "${sdkPath}\MakeAppx.exe"
$signTool = "${sdkPath}\SignTool.exe"
$srcPath = "${currentPath}\${srcFolder}"
$bundlePath = "${currentPath}\${setupBundleFolder}"
$appxPath = "${bundlePath}\WPFWindows_${appxVersion}_x86"
$appxFile = "${appxPath}.appx"
$appxBundleFile = "${appxPath}.appxbundle"
$pfxFile = "${currentPath}\${projectFolder}\myApp_StoreKey.pfx"
& $makePri createconfig /pr $srcPath /cf $srcPath\priconfig.xml /dq en-US /o
& cd $srcPath
& $makePri new /pr $srcPath /cf $srcPath\priconfig.xml /o
& cd ../../../
& $makeAppx pack /v /h sha256 /d $srcPath /p $appxFile
& $signTool sign /fd SHA256 /a /f $pfxFile $appxFile
& $makeAppx bundle /bv $appxVersion /d $bundlePath /p $appxBundleFile
& $signTool sign /fd SHA256 /a /f $pfxFile $appxBundleFile</code></pre>最後建立好 *.appx 或是 *.appxbundle 時,要記得做 <a href="https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/windows-desktop-bridge-app-tests" target="_blank">Windows Desktop Bridge app tests</a>。<br />
======<br />
這篇把根據微軟的教學補充上我自己踩過的雷,因爲建立好的 *.msix 或 *.appx 竟然在送到 Store 審核時檢查的條件不同,所以去看了 <a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-root#build-an-msix-from-an-existing-app-installer" target="_blank">Supported OS versions for installed packages</a> 才知道兩者在 Windows 10 版本的要求有不同。<br />
希望對大家有幫助,謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://blogs.msdn.microsoft.com/appconsult/2017/02/21/an-easier-way-to-generate-the-packages-for-a-desktop-bridge-converted-app/" target="_blank">An easier way to generate the packages for a Desktop Bridge converted app</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2017/08/28/package-a-net-desktop-application-using-the-desktop-bridge-and-visual-studio-preview/" target="_blank">Package a .NET desktop application using the Desktop Bridge and Visual Studio Preview</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/msix/package/sign-app-package-using-signtool" target="_blank">Sign an app package using SignTool</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool" target="_blank">Create an app package with the MakeAppx.exe tool</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion#generate-a-windows-app-package" target="_blank">Package a desktop app manually</a></li>
<li><a href="https://blog.delegate.at/2018/04/15/how-to-use-the-desktop-bridge-to-create-an-appx-package-for-xaf.html" target="_blank">How to use the Desktop Bridge to create an appx package for XAF</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-packaging-dot-net" target="_blank">Package a desktop app from source code using Visual Studio</a></li>
<li><a href="https://github.com/Microsoft/msix-packaging" target="_blank">MSIX Packaging SDK</a></li>
<li><a href="https://www.youtube.com/watch?v=FKCX4Rzfysk" target="_blank">MSIX: Inside and Out : Build 2018</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-packaging-dot-net" target="_blank">Package an app by using Visual Studio (Desktop Bridge)</a></li>
<li><a href="https://gist.github.com/StevenLiekens/cae70cce25344ba47b86" target="_blank">A list of all <target /> tags that ship with MSBuild / Visual Studio</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/msix/package/create-app-package-with-makeappx-tool" target="_blank">Create an app package with the MakeAppx.exe tool</a></li>
<li><a href="https://devblogs.microsoft.com/dotnet/introducing-net-core-windows-forms-designer-preview-1/" target="_blank">Introducing .NET Core Windows Forms Designer Preview 1</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/app-resources/build-resources-into-app-package" target="_blank">Build resources into your app package, instead of into a resource pack</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extensions#start-a-win32-process-from-a-universal-windows-platform-uwp-app" target="_blank">Start a Win32 process from a Universal Windows Platform (UWP) app</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-51990223849814153932019-10-05T16:21:00.000+08:002019-10-05T16:21:54.675+08:00[Flutter] 學習做 Page 之間滑動時淡入淡出圖片學習利用 <a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 搭配 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 做出滑動式前後 Page 淡入淡出的效果。<br />
<a name='more'></a><br />
有些 App 在第一次啓動時顯示一系列的畫面,提供教學或説明該 App 有什麽功能,例如:<br />
<a href="https://1.bp.blogspot.com/-woFJ15DZK8c/XZNfylU7rHI/AAAAAAAABRQ/7d2qNp8lhXgMPO9ARg_6BfFhpsb4W9ObwCLcBGAsYHQ/s1600/demo2.gif" imageanchor="1"><img src="https://1.bp.blogspot.com/-woFJ15DZK8c/XZNfylU7rHI/AAAAAAAABRQ/7d2qNp8lhXgMPO9ARg_6BfFhpsb4W9ObwCLcBGAsYHQ/s400/demo2.gif" width="225" height="400" data-original-width="900" data-original-height="1600" /></a><br />
剛好最近也有做到類似的需求,因此,我藉由找到幾篇教學來説明如何做到。<br />
<br />
要做到多個 Page 之間活動切換,可以使用 <a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 來做到。<br />
<br />
説明幾個重要的地方:<br />
<ul><li><b><a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a></b><div>提供整頁畫面可滑動切換的基本元件,每一頁都是相同大小。<br />
適合一些結構比較簡單的情況使用,例如:多張照片滑動瀏覽,或是廣告版位的自動滑動等。<br />
可搭配 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 控制在滑動時的移動效果(offset)或是調整顯示的大小或坐標等。<br />
例如下面簡單的例子:<br />
<pre class="code prettyprint"><code class="java">PageView(
children: <widget>[
Container(
color: Colors.yellow,
),
Container(
color: Colors.blue,
),
Container(
color: Colors.deepPurple,
),
],
)</code></pre><a href="https://2.bp.blogspot.com/-ubx1iwyORxk/XZNTUTKQzII/AAAAAAAABQw/HfVegY7KuEsTfOEmQx_HZakJ9QisvabzwCLcBGAsYHQ/s1600/demo1.gif" imageanchor="1" ><img src="https://2.bp.blogspot.com/-ubx1iwyORxk/XZNTUTKQzII/AAAAAAAABQw/HfVegY7KuEsTfOEmQx_HZakJ9QisvabzwCLcBGAsYHQ/s320/demo1.gif" width="180" height="320" data-original-width="900" data-original-height="1600" /></a><br />
幾個屬性可以設定來做一些調整:<br />
<ul><li><a href="https://api.flutter.dev/flutter/widgets/PageView/reverse.html" target="_blank">reverse</a>:設定是否要 Scroll 的方向是否反過來;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageView/scrollDirection.html" target="_blank">scrollDirection</a>:設定 Scroll 是要左右滑動或是上下滑動;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageView/pageSnapping.html" target="_blank">pageSnapping</a>:設定滑動時是否自動下一頁,如果設定 false,在滑動時會卡住前後一頁;</ul>在 PageView 在顯示時只會顯示目前要顯示的 Page,當滑動時才會根據滑動的 offset 決定要生成前面或後面的 Page。</br> 例如:假設 PageView 加入了 A,B,C,D 四個 Page,首先會顯示 A,如果往左邊滑動時,PageView 會生成:A -> half of B & half of A -> B; 假設在 B 往右邊滑動時,PageView 會生成: B-> half of A & half of B -> A;</br> 而這樣的邏輯剛好可以搭配 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 來處理,做到我們要的效果。 </div></li>
<li><b><a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a></b><div>讓我們可以操作 PageView 中每一頁的顯示,例如:讓我們在根據畫面(viewport size 的增量)控制 offset。<br />
幾個重點屬性與方法:<br />
<ul><li><a href="https://api.flutter.dev/flutter/widgets/PageController/initialPage.html" target="_blank">initialPage</a>:設定第一個要顯示的 Page;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageController/keepPage.html" target="_blank">keepPage</a>:當 scrollable 被重建時回復到現在的 Page;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageController/page.html" target="_blank">page</a>:取得現在顯示的 Page;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageController/animateToPage.html" target="_blank">animateToPage</a>:設定動畫讓 PagView 從現在的 Page 移動到特定的 Page;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/PageController/jumpToPage.html" target="_blank">jumpToPage</a>:設定移動到特定的 Page;</li>
<li><a href="https://api.flutter.dev/flutter/widgets/ScrollController/offset.html" target="_blank">offset</a>:代表 scrollable widget 目前 scroll 的 offset;其值為 <span class="inline-code">double get offset => position.pixels;</span> 這個值搭配 PageView 設定 <a href="https://api.flutter.dev/flutter/widgets/PageView/scrollDirection.html" target="_blank">scrollDirection</a> 的方向代表不同的對象;<br />
offset 值會從 0 ~ Width*(N-1),例如:<a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 設定 width 是 200,有 3 個 Page,那就是 0 ~ 200*(3-1),每翻過一個新的 Page,offset 就加上 width 直到最後。<br />
</ul>PageController 的使用方式可簡單參考:<a href="https://api.flutter.dev/flutter/widgets/PageController-class.html#widgets.PageController.1" target="_blank">Sample</a>。 </div></li>
</ul><br />
接著參考 <a href="https://juejin.im/post/5c0792d6f265da61137f0e88" target="_blank">Flutter之使用PageView实现图片预览视差效果</a> 做出如上圖在切換 Page 時做出 Page 在相同位置做淡入淡出效果。<br />
裏面有提到幾個重點:<br />
<ul><li>需要 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 的 offset 來計算每個 Page 在移動距離佔整個畫面的比例:<span class="inline-code">var pageOffset = controller.offset / width</span>;(如果您的 Widget 是有固定的寬度,那就是 Page 移動距離佔指定寬度的比例)。<br />
同上面所說 offset 代表是目前 PageController 移動的位置,需要去除以寬度才會知道當前的 Page 移動的比例;</li>
<li>計算出目前移動時左邊的畫面(<span class="inline-code">var currentLeftPageIndex = pageOffset.floor();</span>)是誰,相反地如果您的滑動是垂直的話,那就是計算您上面的畫面是誰;<br />
根據 <a href="https://juejin.im/post/5c0792d6f265da61137f0e88" target="_blank">Flutter之使用PageView实现图片预览视差效果</a> 的介紹,翻頁時最多只能看到兩頁,例如:第 0 頁翻到第 1 頁時,只會看到第 0 跟第 1 頁各一半,再翻過去後只剩下第 1 頁。所以計算 currentLeftPageIndex 再翻頁時就是第 0 頁以此類推。</li>
<li><span class="inline-code">var currentPageOffsetPercent = pageOffset - currentLeftPageIndex;</span> 利用當前頁的 offset 減去左邊頁來得到當前左頁的偏移值,值在 0 ~ 1 之間。代表從第 0 頁翻到第 1 頁中間變量的過程,這個值也是用來做淡入淡出效果的依據。</li>
<li>因爲要固定 Page 的位置做淡入淡出效果,所以要設定被翻出的 Page 為負數的偏移值,如:<span class="inline-code">(pageOffset - index) * width</span>。</li>
</ul>利用完整的程式碼來說明:<br />
<pre class="prettyprint"><code class="java">PageController _controller;
var pageOffset = 0.0;
var screenWidth = 0.0;
var images = [
'http://0rz.tw/y2yyL',
'http://0rz.tw/RTyOI',
'http://0rz.tw/GZfs8',
'http://0rz.tw/myTYd',
'http://0rz.tw/HuKP2'
];
@override
void initState() {
super.initState();
_controller = PageController(initialPage: 0);
// 監聽 PageController 的 Scroller 變化
_controller.addListener(_offsetChanged);
}
void _offsetChanged() {
// 每次的移動都重新計算對應的偏移值與特效
setState(() {
pageOffset = _controller.offset / screenWidth;
});
}
@override
Widget build(BuildContext context) {
// 預設使用螢幕的寬度
screenWidth = MediaQuery.of(context).size.width;
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: PageView.builder(
controller: _controller,
itemCount: images.length,
itemBuilder: (context, index) {
// 計算每次異動時左邊的 Page 是哪個 index
var currentLeftPageIndex = pageOffset.floor();
// 計算現在畫面 Offset 佔的比例
var currentPageOffsetPercent = pageOffset - currentLeftPageIndex;
// 加入移動的特效
return Transform.translate(
// 因爲是水平滑動,所以設定 offset 的 X 值,因爲 Page 固定不動
// 所以要先用 pageOffset 減去 index 得到 負數
// 如果是垂直滑動,請設定 offset 的 Y 值
offset: Offset((pageOffset - index) * screenWidth, 0),
// 加入調整透明度效果
child: Opacity(
// 如果現在左邊的 index 等於正要建立的 index,則讓它透明度變淡,因爲它要退出畫面了
// 相反地是要顯示,則使用原本的 currentPageOffsetPercent
opacity: currentLeftPageIndex == index
? 1 - currentPageOffsetPercent
: currentPageOffsetPercent,
child: Container(
decoration: BoxDecoration(
image: DecorationImage(
image: NetworkImage(images[index]),
fit: BoxFit.cover))),
),
);
},
));
}</code></pre>效果如下圖:<br />
<a href="https://2.bp.blogspot.com/-x6zLWZW8G9k/XZg3d2CHNCI/AAAAAAAABR0/heOWajd28Zgv8Smi-TYrVUiHyaEimoDrACLcBGAsYHQ/s1600/example.gif" imageanchor="1" ><img src="https://2.bp.blogspot.com/-x6zLWZW8G9k/XZg3d2CHNCI/AAAAAAAABR0/heOWajd28Zgv8Smi-TYrVUiHyaEimoDrACLcBGAsYHQ/s400/example.gif" width="225" height="400" data-original-width="900" data-original-height="1600" /></a><br />
<br />
想要<b>加入 indicator</b> 顯示 <a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 到哪一個 Page 呢?<br />
可以利用 <a href="https://gist.github.com/collinjackson/4fddbfa2830ea3ac033e34622f278824" target="_blank">PageView example with dots indicator</a> 的範例,搭配上方建立的 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 來配合,所以調整程式碼把 <a href="https://gist.github.com/collinjackson/4fddbfa2830ea3ac033e34622f278824" target="_blank">PageView example with dots indicator</a> 建立的 indicator widget 加入到上方範例:<br />
<pre class="prettyprint"><code class="java">Stack(
alignment: Alignment.topCenter,
children: [
PageView.builder(
controller: _controller,
itemCount: images.length,
itemBuilder: (context, index) {
...
},),
// 加入 indicator, 並使用相同的 PageController
DotsIndicator(
color: Colors.white,
itemCount: images.length,
controller: _controller,
)
],));</code></pre>上方程式利用 <a href="https://api.flutter.dev/flutter/widgets/Stack-class.html" target="_blank">Stack</a> 堆疊兩個 widget 做出效果,並在 DotIndicator 使用 <a href="https://api.flutter.dev/flutter/widgets/PageController-class.html" target="_blank">PageController</a> 顯示的是那一個 index:<br />
<pre class="prettyprint"><code class="java">double selectedness = Curves.easeOut.transform(
max(
0.0,
1.0 - ((controller.page ?? controller.initialPage) - index).abs(),
),);</code></pre><br />
另外,如果要做到每個 Page 翻頁時,聯動另一個 <a href="https://api.flutter.dev/flutter/widgets/ListView-class.html" target="_blank">ListView</a> 做切換,要怎麼做呢?<br />
需要搭配其他 Widgets:<br />
<ul><li><a href="https://api.flutter.dev/flutter/widgets/IgnorePointer-class.html" target="_blank">IgnorePointer</a><div>用來包裝 Widget 忽略其原有的事件。<br />
例如:範例需要一個 <a href="https://api.flutter.dev/flutter/widgets/ListView-class.html" target="_blank">ListView</a>,它本身有 scroll 功能,不應該跟 <a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 衝突,所以需要它來包裝;</div></li>
<li><a href="https://api.flutter.dev/flutter/widgets/ScrollController-class.html" target="_blank">ScrollController</a><div>負責保存 scroll 狀態,讓具有 scroll 功能的 widget 可以移動或紀錄目前的位置。<br />
例如:利用 <a href="https://api.flutter.dev/flutter/widgets/IgnorePointer-class.html" target="_blank">IgnorePointer</a> 取消了 <a href="https://api.flutter.dev/flutter/widgets/ListView-class.html" target="_blank">ListView</a> 的互動,因此需要加入它來移動到指定的項目;</div></li>
</ul>利用程式碼來說明:<br />
<pre class="prettyprint"><code class="java">final ScrollController _scrollController = ScrollController();
var texts = ['GOW 1', 'GOW 2', 'GOW 3', 'GEARS OF WAR JUDGMENT', 'GEARS OF WAR:ULTIMATE EDITION'];
void _offsetChanged() {
setState(() {
// 利用 PageController.offset 來移動
_scrollController.jumpTo(_controller.offset);
});
}
Scaffold(
appBar: AppBar(
title: Text(widget.title)),
body: Stack(
alignment: AlignmentDirectional.bottomStart,
children: [
PageView.builder(
.....
),
// Indicator
Padding(
padding: const EdgeInsets.only(bottom: 30),
child: DotsIndicator(
color: Colors.white,
itemCount: images.length,
controller: _controller,
)),
// 利用 IgnorePointer 忽略 ListView 的滑動
IgnorePointer(
child: ListView.builder(
// 改利用 ScrollController 來操作 ListView
controller: _scrollController,
scrollDirection: Axis.horizontal,
itemCount: texts.length,
itemBuilder: (context, index) {
return Container(
alignment: Alignment.bottomLeft,
// 設定 width 與 Page 一致
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(left: 10, bottom: 50),
child: Text(
texts[index],
style: TextStyle(fontSize: 20, color: Colors.white),
));
},
))
],
));
</code></pre><span class="inline-code">_scrollController.jumpTo(_controller.offset);</span> 代表 <a href="https://api.flutter.dev/flutter/widgets/ListView-class.html" target="_blank">ListView</a> 原理跟 <a href="https://api.flutter.dev/flutter/widgets/PageView-class.html" target="_blank">PageView</a> 一樣,offset 代表目前 scroll 移動的距離,所以需要把每一個 Item 的寬度設定跟 Page 一樣(螢慕寬度)。 <br />
效果如下:<br />
<a href="https://3.bp.blogspot.com/-i4w1NrzL5QU/XZhReMsWdyI/AAAAAAAABSA/3N69QXlaz6QL_4k-7q67URWttjHfrb92wCLcBGAsYHQ/s1600/fin.gif" imageanchor="1" ><img src="https://3.bp.blogspot.com/-i4w1NrzL5QU/XZhReMsWdyI/AAAAAAAABSA/3N69QXlaz6QL_4k-7q67URWttjHfrb92wCLcBGAsYHQ/s400/fin.gif" width="225" height="400" data-original-width="900" data-original-height="1600" /></a><br />
<br />
範例程式,可以到 <a href="https://github.com/poumason/flutter_samples/tree/master/pageviewer_sample" target="_blank">pageviwer_sample</a> 下載,如果有問題歡迎直接開 issues 給我。<br />
======<br />
開始學習 Flutter 順手記錄開發時遇到的問題與找到的解決方法,盡可能用自己的話説明。希望對大家有所幫助,謝謝。<br />
<br />
<b>References</b><br />
<ul><li><a href="https://juejin.im/post/5c0792d6f265da61137f0e88" target="_blank">Flutter之使用PageView实现图片预览视差效果</a></li>
<li><a href="https://medium.com/flutter-community/synchronising-widget-animations-with-the-scroll-of-a-pageview-in-flutter-2f3475fcffa3" target="_blank">Synchronising widget animations with the scroll of a PageView in Flutter</a></li>
<li><a href="https://gist.github.com/collinjackson/4fddbfa2830ea3ac033e34622f278824" target="_blank">PageView example with dots indicator</a></li>
<li><a href="https://xiaozhuanlan.com/topic/2867431590" target="_blank">用 Flutter 实现 PageView 指示器</a></li>
<li><a href="https://flutterawesome.com/build-page-indicators-for-the-pageview/" target="_blank">Build page indicators for the PageView</a></li>
<li><a href="https://medium.com/@diegoveloper/flutter-lets-know-the-scrollcontroller-and-scrollnotification-652b2685a4ac" target="_blank">Flutter : let’s know the ScrollController and ScrollNotification</a></li>
<li><a href="https://stackoverflow.com/questions/43485529/programmatically-scrolling-to-the-end-of-a-listview" target="_blank">Programmatically scrolling to the end of a ListView</a></li>
<li><a href="https://medium.com/jlouage/flutter-boxdecoration-cheat-sheet-72cedaa1ba20" target="_blank">Flutter — BoxDecoration Cheat Sheet</a></li>
<li><a href="https://lizhaoxuan.github.io/2019/01/02/Flutter-%E4%BD%A0%E8%BF%98%E5%9C%A8%E6%BB%A5%E7%94%A8StatefulWidget%E5%90%97/" target="_blank">Flutter-你还在滥用StatefulWidget吗</a></li>
<li><a href="https://medium.com/jastzeonic/flutter-%E7%9A%84%E9%82%A3%E4%B8%80%E5%85%A9%E4%BB%B6%E4%BA%8B-layout-53e0691b127d" target="_blank">Flutter 的那一兩件事 — Layout</a></li>
<li><a href="https://stackoverflow.com/questions/52489458/how-to-change-status-bar-color-in-flutter" target="_blank">How to change status bar color in Flutter?</a></li>
<li><a href="https://zocada.com/drawing-custom-shapes-in-flutter-using-custompainter/" target="_blank">Drawing Custom Shapes in Flutter using CustomPainter</a></li>
<li><a href="https://stackoverflow.com/questions/45684367/flutter-drawing-a-rectangle-in-bottom" target="_blank">Flutter - Drawing a rectangle in bottom</a></li>
<li><a href="https://medium.com/@auwit0205/flutter-%E7%B0%A1%E6%98%93%E5%8B%95%E7%95%AB-619b213d2ec" target="_blank">Flutter 簡易動畫</a></li>
<li><a href="https://www.saiguerrilla.com/flutter-%E6%95%99%E5%AD%B8-%E5%88%9D%E8%A6%8Bwidget%E6%A6%82%E5%BF%B5/" target="_blank">初見 Widget 概念</a></li>
<li><a href="https://flutter.dev/docs/cookbook/animation/opacity-animation" target="_blank">Fade a widget in and out</a></li>
<li><a href="https://medium.com/flutter-community/flutter-animated-series-animated-opacity-c11137883a8d" target="_blank">Flutter Animated Series : Animated Opacity</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-46001741419916784552019-08-15T01:56:00.001+08:002019-08-15T01:56:35.075+08:00學習 UWP 的 Automation 做自動化測試自己開發的 App 每次都要人工測試,速度慢又重複浪費時間,根據 <a href="https://docs.microsoft.com/en-us/visualstudio/test/use-ui-automation-to-test-your-code?view=vs-2019" target="_blank">Use Coded UI test to test your code</a> 介紹學習自動化測試 App。<br />
<a name='more'></a><br />
根據 <a href="https://docs.microsoft.com/en-us/visualstudio/test/use-ui-automation-to-test-your-code?view=vs-2019" target="_blank">Use Coded UI test to test your code</a> 説明在 Visual Studio 2019 之後不再支援 Coded UI Test,需改用 <a href="https://github.com/Microsoft/WinAppDriver" target="_blank">Appium with WinAppDriver</a> 測試 desktop 與 UWP apps,如果是 Xamarin 則利用 <a href="https://docs.microsoft.com/en-us/appcenter/test-cloud/uitest/" target="_blank">Xamarin.UITest</a> 搭配 NUnit test framework 測試 iOS 與 Android apps。<br />
<br />
<b><a href="https://github.com/Microsoft/WinAppDriver" target="_blank">Windows Application Driver</a></b><br />
支援在 Windows 10 上對 UWP, WinForms, WPF 與傳統的 Win32 應用程式進行 UI 自動化測試。<br />
WinAppDriver 搭配 Appium 建立一個 local server (預設 IP address: 127.0.0.1, port: 4723),藉由對 <a href="http://appium.io/" target="_blank">Appium</a> 下指令 (HTTP Messages/APIs) 到 <a href="https://www.seleniumhq.org/projects/webdriver/" target="_blank">WebDriver</a> 並轉換 Windows 搜尋 UI Controls 與對應的事件。概念如下:<br />
<a href="https://1.bp.blogspot.com/-9UzjHTmFPoI/XRcjVP9oQRI/AAAAAAAABPE/2t2heKK6VY4qhROSfpyY9R_KJQUxjBaGgCLcBGAs/s1600/Untitled.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="475" data-original-width="1531" height="198" src="https://1.bp.blogspot.com/-9UzjHTmFPoI/XRcjVP9oQRI/AAAAAAAABPE/2t2heKK6VY4qhROSfpyY9R_KJQUxjBaGgCLcBGAs/s640/Untitled.png" width="640" /></a><br />
可參考 <a href="https://github.com/Microsoft/WinAppDriver#installing-and-running-windows-application-driver" target="_blank">Installing and Running Windows Application Driver</a> 將 Windows Application Driver 安裝到 Windows 10 中,並特別注意安裝之後的目錄在:<span class="inline-code">Run WinAppDriver.exe from the installation directory (E.g. C:\Program Files (x86)\Windows Application Driver)</span>。<br />
您可以使用方式改變監聽的 port ,如下範例:<br />
<pre class="prettyprint"><code class="csharp">WinAppDriver.exe 4727
WinAppDriver.exe 10.0.0.10 4725
WinAppDriver.exe 10.0.0.10 4723/wd/hub</code></pre>詳細可參考 <a href="https://github.com/Microsoft/WinAppDriver#installing-and-running-windows-application-driver" target="_blank">Installing and Running Windows Application Driver</a>。<br />
<br />
根據 <a href="https://docs.microsoft.com/en-us/visualstudio/test/set-a-unique-automation-property-for-windows-store-controls-for-testing?view=vs-2019" target="_blank">Set a unique automation property for UWP controls for testing</a> 介紹,要做到 UI auto testing 需要在 App 的 UI Controls 加入 <b>AutomationProperties</b> 的設定:<br />
<ul><li>AutomationProperties.AutomationId</li>
<li>AutomationProperties.Name</li>
</ul>可以選擇設定 AutomationId 或是 Name 來使用,如果您在 control 裏面沒有設定的話,例如:<br />
<pre class="prettyprint"><code class="xml"><Button Name="ButtonX" />
<Button Content="ButtonZ" /></code></pre>這兩者都是隱性的處理,AutomationId 會自動使用 Control 的 Name,而 AutomationProperties.Name 則使用 Content 的内容;<br />
但是我個人建議給明確的指定,在寫測試時會比較方便,如下:<br />
<pre class="prettyprint"><code class="xml"><Button AutomationProperties.AutomationId="ButtonY" />
<Button AutomationProperties.Name="ButtonZ" /></code></pre>任何的 XAML Controls 都可以加入 AutomationProperties,例如 ListView 的 ListItem 可以在 binding 根據資料加上 AutomationId,例如:<br />
<pre class="prettyprint"><code class="xml"><ListBox Name="listBox1" ItemsSource="{Binding Source={StaticResource employees}}">
<ListBox.ItemTemplate>
<DataTemplate>
<Button Content="{Binding EmployeeName}" AutomationProperties.AutomationId="{Binding EmployeeID}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox></code></pre>如果您想找到是 ListView 中的任何一筆資料的話,可改用 class name 的方式,例如:<span class="inline-code">FindElementsByClassName</span>。<br />
<br />
安裝好 WinAppDriver 與設定 AutomationProperties 後,接著<b>建立 Unit Test Project 並加入 Appium.WebDriver 的 Nuget</b>,來與 Windows Application Driver 進行 UI auto testing。<br />
<ol><li>建立一個 .NET Framework 的 Unit Test Project,並加入 <a href="https://github.com/appium/appium-dotnet-driver" target="_blank">Appium.WebDriver</a> 的 Nuget;</li>
<li>建立管理 <b>WindowsDriver<WindowsElement></b> 管理 Session,並設定測試的進入點:<div><b>UWP</b><br />
<pre class="prettyprint"><code class="csharp">// 指定 AUMID 與 Appium 建立的 local server
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", "{AUMID}");
AlarmClockSession = new WindowsDriver<windowselement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
</code></pre>有關於 AUMID 的取得可以從 <a href="https://docs.microsoft.com/en-us/windows/configuration/find-the-application-user-model-id-of-an-installed-app" target="_blank">Find the Application User Model ID of an installed app</a> 找到方法。<br />
<b>Classic Windows Application</b><br />
<pre class="prettyprint"><code class="csharp">// Launch Notepad
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", @"C:\Windows\System32\notepad.exe");
appCapabilities.SetCapability("appArguments", @"MyTestFile.txt");
appCapabilities.SetCapability("appWorkingDir", @"C:\MyTestFolder\");
NotepadSession = new WindowsDriver<windowselement>(new Uri("http://127.0.0.1:4723"), appCapabilities);
</code></pre>使用 application 的完整執行路徑,可搭配 appArguments 傳入啓動參數與設定執行的目錄 appWorkingDir。<br />
其他啓動時的參數,可參考:<a href="https://github.com/Microsoft/WinAppDriver#supported-capabilities" target="_blank">Supported Capabilities</a>。</div></li>
<li>WindowsDriver<WindowsElement> 代表建立與 Appium 的連綫,需通過它向 Appium 下達指令,詳細範例:<pre class="prettyprint"><code class="csharp">public class RadioSession
{
private const string WinAppDriverUrl = "http://127.0.0.1:4723";
private const string AppId = "{AUMID}";
public WindowsDriver<windowselement> Session { get; private set; }
public RadioSession()
{
// 啓動 App 並建立 session
DesiredCapabilities appCapabilities = new DesiredCapabilities();
appCapabilities.SetCapability("app", AppId);
Session = new WindowsDriver<windowselement>(new Uri(WinAppDriverUrl), appCapabilities);
// 檢查是否有正確建立 session
Assert.IsNotNull(Session);
Assert.IsNotNull(Session.SessionId);
// 設定搜尋 element 的 timeout 時間,避免沒有回應時整個卡住
Session.Manage().Timeouts().ImplicitlyWait(TimeSpan.FromSeconds(3));
}
}</code></pre></li>
<li>舉例找到下圖 ListView 中的任何一個 Item 並點擊它:<div><a href="https://3.bp.blogspot.com/-H8nSk5TzupE/XVQ3HZUNnRI/AAAAAAAABP4/2soMp2ioUq4JL32ii_YIBBTpcPybuN4tgCLcBGAs/s1600/sample1.png" imageanchor="1" ><img border="0" src="https://3.bp.blogspot.com/-H8nSk5TzupE/XVQ3HZUNnRI/AAAAAAAABP4/2soMp2ioUq4JL32ii_YIBBTpcPybuN4tgCLcBGAs/s400/sample1.png" width="313" height="400" data-original-width="502" data-original-height="642" /></a></div>可利用 <b><a href="https://docs.microsoft.com/en-us/windows/win32/winauto/inspect-objects" target="_blank">Inspect.exe</a></b> 路徑: <span class="inline-code">C:\Program Files (x86)\Windows Kits\10\bin\{version}\{platform}</span> 找到 ListView 的名字。再提醒,XAML 的 control 要記得設定 <span class="inline-code">AutomationProperties</span> 或是給與 <span class="inline-code">x:Name</span>。範例程式如下:<br />
<pre class="prettyprint"><code class="csharp">[TestClass]
public class UnitTest1
{
// 建立 Session 來保持與 Appium 互動
private readonly RadioSession session = new RadioSession();
[TestMethod]
public void TestMethod1()
{
// 找到 ListView
WindowsElement gridView = session.Session.FindElementByAccessibilityId("radioGridView");
// 找到任何一個 Item
var items = gridView.FindElementsByClassName("GridViewItem");
Assert.IsTrue(items.Count > 0);
// 點擊任何一個 Item
items[0].Click();
}
}</code></pre></li>
</ol>做完上面的步驟您已經可以對 App 進行 UI Test 了。<br />
在搜尋 Elements 時可以參考 <a href="https://github.com/microsoft/WinAppDriver#supported-locators-to-find-ui-elements" target="_blank">Supported Locators to Find UI Elements</a> 的介紹有更多的方式。<br />
<br />
<b>補充</b><br />
<ul><li>如果您的專案組合了 CEFSharp 的話,可以參考 <a href="https://github.com/cefsharp/CefSharp/wiki/General-Usage#cefsettings-and-browsersettings" target="_blank">CefSettings and BrowserSettings</a> 的做法,開啓 <b>RemoteDebuggingPort</b> 搭配 <a href="https://chromedriver.chromium.org/" target="_blank">ChromeDriver - WebDriver for Chrome</a>的方式來測試裏面的内容。</li>
<li>如果拿到一個 App 找不到 AutomationId 時,可以搭配 <b>Inspect.exe</b> 來搜尋 App 中想要元件的 Id 或 Name。<a href="https://github.com/Microsoft/WinAppDriver/wiki/Frequently-Asked-Questions#how-can-i-try-out-winappdriver-functionality" target="_blank">How can I try out WinAppDriver functionality?</a></li>
<li>Test Project 幾個需要注意的地方:<ul><li>ClassInitialize 與 ClassCleanup<div>每一個 TestClass 只能有一個 ClassInitialize ,它會在所有 TestMethods 被執行前優先處理;而 ClassCleanup 則是在所有 TestMethods 被完成後才會執行;常用來做一些測試前參數的準備與測試後面還原。</div></li>
<li>TestInitialize 與 TestCleanup<div>TestInitialize 會在每個 TestMethods 被執行前處理;TestCleanup 則是在每個 TestMethods 被完成後面執行。</div></li>
</ul></li>
</ul>======<br />
如果您是個人開發者也許不太會用到該篇的内容,但我建議還是能參考著使用。<br />
因爲 UI Auto testing 導入之後,讓手動測試變成自動化,可以測試更多比較偶發的行爲。<br />
本篇内容希望對於要入門 Windows apps 做 UI Auto testing 的人有所幫助,謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://appium.io/docs/en/drivers/windows/" target="_blank">Appium The Windows Driver</a></li>
<li><a href="https://github.com/Microsoft/WinAppDriver" target="_blank">Windows Application Driver</a></li>
<li><a href="https://channel9.msdn.com/Shows/Visual-Studio-Toolbox/UI-Tests-for-Desktop-and-UWP-Apps" target="_blank">UI Tests for Desktop and UWP Apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/visualstudio/test/use-ui-automation-to-test-your-code?view=vs-2017" target="_blank">Use UI automation to test your code</a></li>
<li><a href="https://docs.microsoft.com/en-us/visualstudio/test/test-uwp-app-with-coded-ui-test?view=vs-2017" target="_blank">Create a coded UI test to test a UWP app</a></li>
<li><a href="https://docs.microsoft.com/en-us/visualstudio/test/set-a-unique-automation-property-for-windows-store-controls-for-testing?view=vs-2017" target="_blank">Set a unique automation property for UWP controls for testing</a></li>
<li><a href="https://www.youtube.com/watch?v=EW6lYxVO2ow" target="_blank">Identify and work with Windows UI element using WinAppDriver</a></li>
<li><a href="https://www.youtube.com/watch?v=rdlK8WZC18o" target="_blank">Improving App Quality with UI Automation</a></li>
<li><a href="https://kkboxsqa.wordpress.com/2019/03/10/windows-automation-with-winappdriver/" target="_blank">Windows Automation 沒有想像中的難</a></li>
<li><a href="https://www.meziantou.net/mstest-v2-test-lifecycle-attributes.htm" target="_blank">MSTest v2: Test lifecycle attributes</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-56457041174715023562019-04-12T00:05:00.002+08:002019-04-12T00:05:07.948+08:00WPF 使用 Windows 10 APIs - 3本篇介紹 Bridge WPF 專案整合 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview" target="_blank">Windows Notification Service(WNS)</a>,並處理相關的事件。<br />
<a name='more'></a><br />
對於 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/windows-push-notification-services--wns--overview" target="_blank">WNS</a> 運作方式,可參考 <a href="https://dotblogs.com.tw/pou/2015/05/27/151413" target="_blank">Universal App - 整合 Windows Notification Service (WNS) for Server</a> 與 <a href="https://dotblogs.com.tw/pou/2015/05/27/151414" target="_blank">Universal App - 整合 Windows Notification Service (WNS) for Client</a> 的介紹,瞭解 WNS 與 UWP 的整合。<br />
<br />
根據 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-desktop-apps" target="_blank">Toast notifications from desktop apps</a> 的介紹,由於 Desktop apps(Desktop Bridge and classic Win32) 因爲架構不同,整合有不同的選擇,但 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-desktop-apps#preferred-option---com-activator" target="_blank">COM activator</a> 來處理 Windows 10 Notifications 是最好的選擇,因爲所有的功能都有支援,如下圖:<br />
<a href="https://3.bp.blogspot.com/-sf2zmWZf8TA/XKAq19h4GDI/AAAAAAAABOA/UDPPqXdOqn04QgTD4Zh_jokuAIQNvvhzwCLcBGAs/s1600/all_options.png" imageanchor="1" ><img border="0" src="https://3.bp.blogspot.com/-sf2zmWZf8TA/XKAq19h4GDI/AAAAAAAABOA/UDPPqXdOqn04QgTD4Zh_jokuAIQNvvhzwCLcBGAs/s640/all_options.png" width="640" height="129" data-original-width="774" data-original-height="156" /></a><br />
加上官方有寫好的 library: <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop" target="_blank"> Send a local toast notification from destkop C# apps</a> 與 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop-cpp-wrl" target="_blank">Send a local toast notification from destkop C++ WRL apps</a> 更方便我們完成整合。<br />
需注意:<br />
<ul><li>Desktop Bridge apps 的整合會更接近 UWP,用戶點擊 toast 時,要能啓動 app 並帶入 toast 中的參數;</li>
<li>Classic Win32 apps 需設定 AUMID 來傳送 toast,或搭配 CLSID 設定捷徑。<div><ul><li>可利用亂數 GUID 來設定 GUID CLSID;</li>
<li>切勿新增 COM Server / COM activator;</li>
<li>可以設定 stub COM CLSID,讓 Action Center 保存您的通知;</li>
<li>只能使用 protocol 類型的 toast,因爲 stub COM CLSID 會中斷其他任何 toast 的啓動。因此要更新 App 讓它支援 protocol 啓動;</li>
</ul></div></li>
</ul>以 Desktop Bridge (WPF) 程式爲例,整合 WNS 需要以下步驟:<br />
<ol><li>建立並完成 Desktop Bridge 的基本設定,可參考<a href="https://dotblogs.com.tw/pou/2018/12/31/184154" target="_blank">WPF 使用 Windows 10 APIs - 1</a>;</li>
<li>修改 WPF 專案檔,加入:<span class="inline-code"><TargetPlatformVersion /></span> 設定預計支援 Windows 10 的最小版本,如下:<pre class="code prettyprint"><code class="XML"><TargetFrameworkVersion>...</TargetFrameworkVersion>
<TargetPlatformVersion>10.0.10240.0</TargetPlatformVersion></code></pre></li>
<li>加入必要的參考:<b>Windows.Data</b> 與 <b>Windows.UI</b>,如下圖:<a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop#step-2-reference-the-apis" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/images/win32-add-windows-reference.png" width="70%" /></a><div>如果您已經加入過 <span class="inline-code">Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmd</span> 與 <span class="inline-code">Windows\Microsoft.NET\Framework\v4.0.30319\System.Runtime.WindowsRuntime.dll</span> 的話,不需要在加入 Windows.UI 與 Window.Data 因爲 Windows.winmd 已經有了。</div></li>
<li>下載 <a href="https://raw.githubusercontent.com/WindowsNotifications/desktop-toasts/master/CS/DesktopToastsApp/DesktopNotificationManagerCompat.cs" target="_blank"> DesktopNotificationManagerCompat.cs file from GitHub</a> 並加入到專案中;該 compact library 簡化複雜 desktop application 整合 notifications 的方式,並加入處理 notifications 與 apps 之間的互動;</li>
<li>實作 activator 處理用戶點擊 toast 時與 App 的互動。<div>延申 <b>NotificationActivator</b> 類別與加入 3 個必要的屬性宣告,並為 App 加入 unique GUID CLSID,GUID 可以是亂數產生的。<br />
CLSID(class identify) 用在向 Action Center 讓他知道收到 toast 對應到哪一個 COM activate。<pre class="code prettyprint"><code class="csharp">// GUID CLSID 必須是唯一值。
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(INotificationActivationCallback))]
[Guid("replaced-with-your-guid-C173E6ADF0C3"), ComVisible(true)]
public class MyNotificationActivator : NotificationActivator
{
public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
{
// 負責處理收到 OnActivated 的事件
}
}</code></pre></div></li>
<li>向 notification platform 注冊,這裏使用 Desktop Bridge 爲例,如果您是 classic Win32 請參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop#step-5-register-with-notification-platform" target="_blank">Register with notification platform</a>:<div>在 <b>Package.appxmanifest</b> 加入 <b>xmlns:com</b> 與 <b>xmlns:desktop</b> 的宣告,並設定 windows.comServer(com extension) 與 windows.toastNotificaiton (desktop extension),兩者要記得使用與上一步的 GUID 一致。<pre class="code prettyprint"><code class="xml"><Package
...
xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="... com desktop">
...
<Applications>
<Application>
...
<Extensions>
<!--Register COM CLSID LocalServer32 registry key-->
<com:Extension Category="windows.comServer">
<com:ComServer>
<!-- YourProject\YourProject.exe 需注意專案輸出的目錄,不可以用 $targetnametoken$.exe -->
<com:ExeServer Executable="YourProject\YourProject.exe" Arguments="-ToastActivated" DisplayName="Toast activator">
<com:Class Id="replaced-with-your-guid-C173E6ADF0C3" DisplayName="Toast activator"/>
</com:ExeServer>
</com:ComServer>
</com:Extension>
<!--Specify which CLSID to activate when toast clicked-->
<desktop:Extension Category="windows.toastNotificationActivation">
<desktop:ToastNotificationActivation ToastActivatorCLSID="replaced-with-your-guid-C173E6ADF0C3" />
</desktop:Extension>
</Extensions>
</Application>
</Applications>
</Package></code></pre></div></li>
<li>注冊 AUMID 與 COM Server:<div>AUMID 的取得可參考:<a href="https://jcutrer.com/windows/find-aumid" target="_blank">Find the AUMID (Application User Model ID) of an installed UWP app</a>,如果您沒有特別設定通常是 <span class="inline-code">{Package family name}!App</span>。<pre class="code prettyprint"><code class="csharp">protected override void OnStartup(StartupEventArgs e)
{
// Register AUMID and COM server (for Desktop Bridge apps, this no-ops)
DesktopNotificationManagerCompat.RegisterAumidAndComServer<MyNotificationActivator>("{AUMID}");
// Register COM server and activator type
DesktopNotificationManagerCompat.RegisterActivator<MyNotificationActivator>();
}</code></pre>需注意:<ul><li>如果抓不到 AUMID,請先安裝一次您的 App;</li>
<li>如果同時支援 Desktop Bridge 與 classic Win32 apps 可以隨時使用 <b>RegisterAumidAndComServer</b> 方法;如果是 Desktop Bridge 只需要在 OnStartup() 時使用;AUMID 在注冊 COM Server 時會一起被記錄在 LocalServer32 的 register key 裏面;</li>
<li>不管是 Desktop Bridge 或 classic Win32 apps 都需要使用 <b>DesktopNotificationManagerCompat.RegisterActivator</b> 注冊 notification action type (例如上面範例的 MyNotificationActivator);</li>
</ul></div></li>
<li><b>DesktopNotificationManagerCompat</b> 包裝了發送 ToastNotifier 幫忙發送 toast 訊息,減少我們自己撰寫 toast 的 XML 造成的錯誤;或者選擇安裝 <a href="https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.Notifications/" target="_blank">Notifications library</a> 也可以方便使用。詳細使用方式可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop#step-7-send-a-notification" target="_blank">Step 7: Send a notification</a>;</li>
<li>在 MyNotificationActivator.OnActivated 與 App.OnStartup 加入處理 notification 的機制:<div>開發過 UWP 熟悉 toast 被用戶點擊時會啓動 app,而存在兩個狀況:app 未開啓時會觸發 OnLaunch;app 已開啓時會觸發 OnActivated。同樣第以 WPF 爲例:<ul><li>app 正開著,會進入:NotificationActivator.OnActivated;</li>
<li>app 未被開啓,會進入 App.OnStartup 事件,並帶入啓動 toast 中夾帶 <span class="inline-code">-ToastActivated</span> 的參數;接著在呼叫 NotificationActivator.OnActivated;</li>
</ul>利用兩段代碼片段來説明:<br />
<pre class="code prettyprint"><code class="csharp">// The GUID CLSID must be unique to your app. Create a new GUID if copying this code.
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(INotificationActivationCallback))]
[Guid("replaced-with-your-guid-a3af-C173E6ADF0C3"), ComVisible(true)]
public class MyNotificationActivator : NotificationActivator
{
public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
{
// 要記得使用 Application.Current.Dispatcher 包裝要操作的 UI 事件;
// 因爲 com activator 是在另一個 thread
Application.Current.Dispatcher.Invoke(() =>
{
// 檢查是否有 window 在前景顯示
OpenWindowIfNeeded();
});
}
private void OpenWindowIfNeeded()
{
// 檢查是否有 window 被開啓,如果沒有則需要建立一個;
// == 0 的狀況會發生在 toast 被點擊但 app 還沒有被開啓的時候;
if (App.Current.Windows.Count == 0)
{
new MainWindow().Show();
}
// 啓動 window 讓 window 被系統 focus 跑到第一個
App.Current.Windows[0].Activate();
// 設定 window 要顯示的大小
App.Current.Windows[0].WindowState = WindowState.Normal;
}
}</code></pre><pre class="code prettyprint"><code class="csharp">protected override void OnStartup(StartupEventArgs e)
{
// 利用 DesktopNotificationManagerCompat 注冊 AUMID 到 COM Server
DesktopNotificationManagerCompat.RegisterAumidAndComServer<mynotificationactivator>("{AUMID}");
// 利用 DesktopNotificationManagerCompat 注冊處理 COM Server 的 Activator
DesktopNotificationManagerCompat.RegisterActivator<mynotificationactivator>();
// 如果 App 被啓動是來自於 toast 被 click 的話,就會帶入 -ToastActivated 的參數
// -ToastActivated 也是我們在 Package.appxmanifest 宣告的 activator 參數
if (e.Args.Contains("-ToastActivated"))
{
// 如果是來自 toast 啓動 app 最後會走進 注冊的 OnActivated 中
// 由於上方的範例改由 OnActivated 判讀是否需要產生 window 這裏就不需要了
}
else
{
// 一般啓動 app 需要顯示 window
// 在 App.xaml 中務移除 StartupUri 以便預設情況下不會創建 window,因爲我們需要自己控制。而且有時候我們只希望在沒有 window 下處理任務。
new MainWindow().Show();
}
base.OnStartup(e);
}</code></pre>需注意 Desktop apps 的 <b>foreground 與 background 啓動的識別機制一樣,都是透過 COM activator 的呼叫</b>。<br />
因此,apps 收到 toast 時要顯示 window 或是當作 worker 只處理任務是由您寫的 code 來決定。<br />
即使設定 toast 中的 <a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/Notifications" target="_blank">ActivationType</a> 是 Background 也無效果。</div></li>
<li>清除 toast 或是指定特定 tag 的 taost:<div><pre class="code prerttyprint"><code class="csharp">// Remove the toast with tag "Message2"
DesktopNotificationManagerCompat.History.Remove("Message2");
// Clear all toasts
DesktopNotificationManagerCompat.History.Clear();</code></pre>建議使用 <a href="https://raw.githubusercontent.com/WindowsNotifications/desktop-toasts/master/CS/DesktopToastsApp/DesktopNotificationManagerCompat.cs" target="_blank">DesktopNotificationHistoryCompat</a> 包裝好的 methods 來操作 toast,省去 AUMID 的使用。</div></li>
</ol><br />
上面介紹讓 Desktop bridge 或 Win32 app 能夠整合 toast,接著繼續介紹整合 Windows Notification Service。<br />
如何拿到 WNS 的 server 端,可參考 <a href="https://dotblogs.com.tw/pou/2015/05/27/151413" target="_blank">Universal App - 整合 Windows Notification Service (WNS) for Server</a>。<br />
下面以 WPF 爲例介紹取得 WNS channel:<br />
<pre class="code prettyprint"><code class="csharp">private async void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
try
{
// 與 UWP 一樣使用 PushNotificationChannelManager 拿到 channel
var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
channel.PushNotificationReceived += Channel_PushNotificationReceived;
Debug.WriteLine(channel.Uri);
}
catch (Exception ex)
{
Debug.WriteLine(ex);
}
}
private void Channel_PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs args)
{
// 可根據自己的需要,處理收到的訊息,但需注意這裏的寫法只有 app 在前景時有用;
// 可以寫到 BackgroundTask 包裝起來;
switch (args.NotificationType)
{
case PushNotificationType.Raw:
break;
case PushNotificationType.Tile:
break;
case PushNotificationType.TileFlyout:
break;
case PushNotificationType.Toast:
break;
}
args.Cancel = true;
}</code></pre>[<b>補充</b>]<br />
<ul><li><a href="https://raw.githubusercontent.com/WindowsNotifications/desktop-toasts/master/CS/DesktopToastsApp/DesktopNotificationManagerCompat.cs" target="_blank">DesktopNotificationManagerCompat</a> 該檔案幫忙寫好處理注冊 COM 與接受訊息時,呼叫 kernal32.dll 來轉發到注冊的 *.exe,並套入既有 Application 的 life cycle。</li>
</ul><br />
[<b>範例程式</b>]<br />
<a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/34-WpfPushNotification" target="_blank">DotblogsSampleCode/Samples/34-WpfPushNotification/</a><br />
======<br />
隨著微軟陸續開發 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/apps-on-arm-x86-emulation" target="_blank">How x86 emulation works on ARM</a> 的方向,加上 Bridge Desktop apps 與 PWA apps 不斷地在 Microsoft Store 上架,發現除了 UWP 之外還有其他開發方式,可以讓 Apps 在 Windows 10 設備上執行。<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-root" target="_blank">Desktop Bridge</a> 是延續企業軟體最好的機制,因此幫忙研究一些可能用到的技術,希望對大家有所幫忙。謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-desktop-apps" target="_blank">Toast notifications from desktop apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop" target="_blank">Send a local toast notification from desktop C# apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/desktop/apiindex/uwp-apis-callable-from-a-classic-desktop-app" target="_blank">UWP APIs callable from a classic desktop app</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/#01bI5gQE8W4WeXEZ.97" target="_blank">Calling Windows 10 APIs From a Desktop Application</a></li>
<li><a href="https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.Notifications/" target="_blank">Microsoft.Toolkit.Uwp.Notifications</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts" target="_blank">Toast content</a></li>
<li><a href="https://github.com/Microsoft/DesktopBridgeToUWP-Samples" target="_blank">Desktop app bridge to UWP Samples</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/02/01/adding-uwp-features-existing-pc-software/" target="_blank">Adding UWP features to your existing PC software</a></li>
<li><a href="https://csharp.christiannagel.com/2018/10/09/desktopbridge/" target="_blank">Desktop Applications with XAML. Part 2: Desktop Bridge</a></li>
<li><a href="https://www.youtube.com/watch?v=RA_VH2m0jaI" target="_blank">Using the Desktop Bridge</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-84728587808335685982019-02-26T10:18:00.000+08:002019-02-26T10:30:54.254+08:00WPF 使用 Windows 10 APIs - 2<a href="https://poumason.blogspot.com/2018/12/wpf-windows-10-apis-1.html" target="_blank">WPF 使用 Windows 10 APIs - 1</a> 介紹如何包裝 WPF 成爲 Bridge App 與如何跟 UWP App 互動,這篇繼續補充 WPF 成爲 Bridge App 後,原本 Win32 程式的特性怎麽對齊。<br />
<a name='more'></a><br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extensions" target="_blank">Integrate your packaged desktop application with Windows 10</a> 介紹重點:<br />
<ul><li><b>讓 WPF 已經被建立的捷徑能繼續使用,例如:Start menu 或 Task bar 上面的捷徑</b>:<div>在 Package.appxmanifest 中補上:<br />
<pre class="code prettyprint"><code class="xml"><Extension Category="windows.desktopAppMigration">
<DesktopAppMigration>
<DesktopApp AumId="[your_app_aumid]" />
<DesktopApp ShortcutPath="[path]" />
</DesktopAppMigration>
</Extension></code></pre><ul><li>windows.desktopAppMigration:來自 <a href="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3" target="_blank">http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities/3</a> 告訴系統在安裝時要如何串聯定義到 AumId 與 ShortcutPath。</li>
<li>AumId (Application User Model ID):是加上驚嘆號和應用程式識別碼的套件系列名稱,例如:<span class="inline-code">{Package family name}!{Applcation's Id}</span>。或是可參考 <a href="https://jcutrer.com/windows/find-aumid" target="_blank">Find the AUMID (Application User Model ID) of an installed UWP app</a> 來取得。它可以被用來做自動化啓動使用,參考 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/automate-launching-uwp-apps" target="_blank">自動化啟動 Windows 10 UWP App</a>。</li>
<li>ShortcutPath:設定原本 WPF 使用的 *.Ink 位置,<a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extensions#example" target="_blank">範例</a>。</li>
</ul></div></li>
<li><b>讓 packaged application 取代原本 desktop application 預設處理的檔案類型</b>:<br />
<div>需要先拿到每一個程式專屬的 <a href="https://docs.microsoft.com/en-us/windows/desktop/shell/fa-progids" target="_blank">programmatic identify (ProgID)</a>,並將它與 file association 綁定在一起。<br />
<pre class="code prettyprint"><code class="xml"><Extension Category="windows.fileTypeAssociation">
<FileTypeAssociation Name="[AppID]">
<MigrationProgIds>
<MigrationProgId>"[ProgID]"</MigrationProgId>
</MigrationProgIds>
</FileTypeAssociation>
</Extension></code></pre><ul><li>windows.fileTypeAssociation 固定值</li>
<li><a href="https://docs.microsoft.com/en-us/windows/desktop/shell/fa-progids" target="_blank">programmatic identifier (ProgID)</a> 代表 App 的唯一識別碼,例如:MyWPFBridge;</li>
<li>MigrationProgId 給 desktop application 當時的 ProgID,它會與設定 Name 整合在一起,例如:oldApp.jpb.a; <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extensions#example-1" target="_blank">範例</a></li>
</ul>而注冊 File Type 的預設處理程式會被注冊在機碼裏面,如:<a href="https://docs.microsoft.com/en-us/windows/desktop/shell/how-to-register-a-file-type-for-a-new-application" target="_blank">How to Register a File Type for a New Application</a>。</div></li>
<li><b>讓用戶對檔案按下右鍵時,可以在 open with 裏面找到 packaged application ,並取代 desktop application</b>;<div>重點是注冊必要的 file extension,設定方式同 UWP 的 <a href="https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/appxmanifestschema/element-filetypeassociation" target="_blank">FileTypeAssociation</a>。<br />
<pre class="code prettyprint"><code class="xml"><Extension Category="windows.fileTypeAssociation">
<FileTypeAssociation Name="[AppID]">
<SupportedFileTypes>
<FileType>"[file extension]"</FileType>
</SupportedFileTypes>
</FileTypeAssociation>
</Extension></code></pre><ul><li>windows.fileTypeAssociation 固定值</li>
<li>FileType 例如: .jpb, .avi 等副檔名</li>
</ul></div></li>
<li><b>為特定的檔案類型增加按下右鍵後,出現多個選項,例如:開啓或列印的範例</b>;<div>一樣在 <span class="inline-code">FileTypeAssocation</span> tag 下加入 <span class="inline-code">SupportedVerb</span>,<br />
由於是用 <a href="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" target="_blank">http://schemas.microsoft.com/appx/manifest/uap/windows10/3</a> 的命名空間,因此這個設定一樣支援 UWP application。如下:<br />
<pre class="code prettyprint"><code class="xml"><Extension Category="windows.fileTypeAssociation">
<FileTypeAssociation Name="[AppID]">
<SupportedVerbs>
<Verb Id="[ID]" Extended="[Extended]" Parameters="[parameters]">"[verb label]"</Verb>
</SupportedVerbs>
</FileTypeAssociation>
</Extension></code></pre><ul><li>Ver 代表在 File Explorer 的右鍵内容要顯示的項目名稱,可搭配 ms-resources 支持多語系。</li>
<li>Id 代表 Ver 的唯一值。<div>如果是 UWP application,該參數會被帶入 activation event args 裏面;如果是 full-trust packaged app,則會收到下面介紹的 parameters。</div></li>
<li>Parameters 代表啓動該 Verb 要帶入的參數;如果是 full-trust packaged application 該參數將會被傳入在程式啓動時。Parameters 可搭配 Verb 客制為自己需的内容,甚至帶入 file path,請利用引號(")做為包裝避免有空白造成錯誤,例如: <span class="inline-code">Parameters="/e "%1""</span>。Parameters 不支援 UWP application。</li>
<li>Extended 該特性只支援用戶先按住 shift 按鈕,再到檔案上按下滑鼠右鍵時,該 Verb 才會顯示。預設是 false 代表用戶在檔案按右鍵就會顯示。</li>
</ul></div></li>
<li><b>支持檔案利用 URL 開啓 packaged application</b>:<div><pre class="code prettyprint"><code class="xml"><Package
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
IgnorableNamespaces="uap, uap3">
<Applications>
<Application>
<Extensions>
<uap:Extension Category="windows.fileTypeAssociation">
<uap3:FileTypeAssociation Name="documenttypes" UseUrl="true" Parameters="%1">
<uap:SupportedFileTypes>
<uap:FileType>.txt</uap:FileType>
<uap:FileType>.doc</uap:FileType>
</uap:SupportedFileTypes>
</uap3:FileTypeAssociation>
</uap:Extension>
</Extensions>
</Application>
</Applications>
</Package></code></pre>其中 UseUrl 如果有設定可以在用戶輸入 URL 時先用 packaged application 打開,而不會先下載。</div></li>
<li><b>在防火墻加入例外你的 App</b>:<div>如果您的 application 用到特殊的網路 Port 需要向防火墻宣告,才能正常使用。使用 <a href="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2" target="_blank">http://schemas.microsoft.com/appx/manifest/desktop/windows10/2</a> 命名空間,只支援 Desktop 環境使用。<br />
<pre class="code prettyprint"><code class="xml"><Package
xmlns:desktop2="http://schemas.microsoft.com/appx/manifest/desktop/windows10/2"
IgnorableNamespaces="desktop2">
<Extensions>
<desktop2:Extension Category="windows.firewallRules">
<desktop2:FirewallRules Executable="Contoso.exe">
<desktop2:Rule Direction="in" IPProtocol="TCP" Profile="all"/>
<desktop2:Rule Direction="in" IPProtocol="UDP" LocalPortMin="1337" LocalPortMax="1338" Profile="domain"/>
<desktop2:Rule Direction="in" IPProtocol="UDP" LocalPortMin="1337" LocalPortMax="1338" Profile="public"/>
<desktop2:Rule Direction="out" IPProtocol="UDP" LocalPortMin="1339" LocalPortMax="1340" RemotePortMin="15"
RemotePortMax="19" Profile="domainAndPrivate"/>
<desktop2:Rule Direction="out" IPProtocol="GRE" Profile="private"/>
</desktop2:FirewallRules>
</desktop2:Extension>
</Extensions>
</Package></code></pre><ul><li>windows.firewallRules 固定值,搭配 <span class="inline-code">FirewallRules</span> 使用</li>
<li>Executable 代表這條規則的名稱,它會被加入到 firewall 的例外清單裏</li>
<li>Direction 設定為 inbound 或 outbound</li>
<li>IPProtocol 設定為 TCP, UDP 或其他 communication protocol</li>
<li>LocalPortMin 代表 local port numbers 的最小值</li>
<li>LocalPortMax 代表 local port numbers 的最大值</li>
<li>RemotePortMin 代表 remote port numbers 的最小值</li>
<li>RemotePortMax 代表 remote port numbers 的最大值</li>
<li>Profile 網路類型:private, public, ... 等</li>
</ul></div></li>
</ul>還有更多整合 File Explorer 與設定 Environment 的部分可以參考:<a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extensions#integrate-with-file-explorer" target="_blank">Integrate with File Explorer</a> 介紹。<br />
<br />
接下來介紹 packaged application 如何與其他 applications 整合。<br />
<ul><li><b>讓 packaged application 支援其他 applications 想要列印時,可以找到您的 application</b>:<div>例如用戶從 notepad 按下列印,列印程式選單裏面會出現您的 application。<pre class="code prettyprint"><code class="xml"><Extension Category="windows.appPrinter">
<AppPrinter
DisplayName="[DisplayName]"
Parameters="[Parameters]" />
</Extension></code></pre><ul><li>DisplayName 代表要顯示的名稱</li>
<li>Parameters 給任何您程式需要的參數,例如:<span class="inline-code">Parameters="/insertdoc %1"</span></li>
</ul></div></li>
<li><b>分享 packaged application 中的自定義 font 給其他 applications</b>:<div><pre class="code prettyprint"><code class="xml"><Extension Category="windows.sharedFonts">
<SharedFonts>
<Font File="[FontFile]" />
</SharedFonts>
</Extension></code></pre>File 指定您 application 放的 font 位置</div></li>
<li><b>從 UWP application 啓動 win32 process</b>:<div><pre class="code prettypring"><code class="xml"><Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap=
"http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10">
...
<Capabilities>
<rescap:Capability Name="runFullTrust"/>
</Capabilities>
<Applications>
<Application>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="fulltrustprocess.exe">
<desktop:FullTrustProcess>
<desktop:ParameterGroup GroupId="SyncGroup" Parameters="/Sync"/>
<desktop:ParameterGroup GroupId="OtherGroup" Parameters="/Other"/>
</desktop:FullTrustProcess>
</desktop:Extension>
</Extensions>
</Application>
</Applications>
</Package></code></pre><ul><li>GroupId 代表要傳送 parameter 的識別值</li>
<li>Parameters 代表要使用的内容</li>
</ul>如果您想要建立一個 UWP 可以呼叫的 win32 元件,可以使用這個方法來宣告使用 full-trust process 的機制,並設定 packaged application 的形執行位置,例如:fulltrustprocess.exe,這樣一來 UWP app 就能直接呼叫使用。<br />
如果您想要讓 UWP application 與 Win32 application 互相溝通,可以建立 app service 來完成,可以參考 <a href="https://poumason.blogspot.com/2018/12/wpf-windows-10-apis-1.html" target="_blank">WPF 使用 Windows 10 APIs - 1</a>。<br />
</div></li>
</ul>上面介紹的内容比較詳細的範例程式,可以參考 <a href="https://github.com/Microsoft/DesktopBridgeToUWP-Samples/tree/master/Samples/DesktopAppTransition" target="_blank">WPF picture viewer with transition/migration/uninstallation</a>。<br />
<br />
在 <a href="https://poumason.blogspot.com/2018/12/wpf-windows-10-apis-1.html" target="_blank">WPF 使用 Windows 10 APIs - 1</a> 介紹幾個常用的 Windows 10 APIs 以及 UWP app 與 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">AppService</a> 的互動 (需要注意 <span class="inline-code">windows.fullTrustProcess</span> 只能在 Desktop 使用),這一篇再補充整合 UWP app 與 Background Task 的使用。<br />
<ul><li>建立一個 windows runtime component 的 Background Task:<pre class="code prettyprint"><code class="csharp">BackgroundTaskDeferral _deferral;
public async void Run(IBackgroundTaskInstance taskInstance)
{
_deferral = taskInstance.GetDeferral();
// 利用 send toast 的方式表示有處理
string msg = $"收到 TimeZoneChanged 的事件,{TimeZoneInfo.Local.DisplayName}";
ToastTemplateType toastTemplate = ToastTemplateType.ToastText02;
XmlDocument toastXml = ToastNotificationManager.GetTemplateContent(toastTemplate);
XmlNodeList toastTextElements = toastXml.GetElementsByTagName("text");
toastTextElements[0].AppendChild(toastXml.CreateTextNode(msg));
toastTextElements[1].AppendChild(toastXml.CreateTextNode(DateTime.Now.ToString()));
ToastNotification toast = new ToastNotification(toastXml);
ToastNotificationManager.CreateToastNotifier().Show(toast);
_deferral.Complete();
}</code></pre></li>
<li>在 WPF 加入 <span class="inline-code">C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmd</span> 與 <span class="inlinr-code">C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\*WindowsRuntime*.dll</span> 的參考,並注冊 Background Task 與 entry point。<pre class="code prettyprint"><code class="csharp">private void RegistBackgroundTask()
{
var taskRegistered = false;
var exampleTaskName = "ExampleBackgroundTask";
foreach (var task in BackgroundTaskRegistration.AllTasks)
{
if (task.Value.Name == exampleTaskName)
{
taskRegistered = true;
break;
}
}
if (taskRegistered)
{
return;
}
var builder = new BackgroundTaskBuilder();
builder.Name = exampleTaskName;
builder.TaskEntryPoint = "MyBackgroundTask.ExampleBackgroundTask";
builder.SetTrigger(new SystemTrigger(SystemTriggerType.TimeZoneChange, false));
builder.AddCondition(new SystemCondition(SystemConditionType.UserPresent));
BackgroundTaskRegistration registResult = builder.Register();
}</code></pre><div></div></li>
<li>在 Windows Application Packaging Project 分別把 WPF 專案與 Windows runtime component 專案加入,並宣告必要的内容:<pre class="code prettyprint"><Proejct>
<PropertyGroup>
<ProjectGuid>fd2d401e-0cd1-48df-9812-cd56909d97cf</ProjectGuid>
<TargetPlatformVersion>10.0.17763.0</TargetPlatformVersion>
<TargetPlatformMinVersion>10.0.15063.0</TargetPlatformMinVersion>
<DefaultLanguage>en-US</DefaultLanguage>
<PackageCertificateKeyFile>WapProjTemplate1_TemporaryKey.pfx</PackageCertificateKeyFile>
<EntryPointProjectUniqueName>..\WPFWithBackgroundTask\WPFWithBackgroundTask.csproj</EntryPointProjectUniqueName>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\WPFWithBackgroundTask\WPFWithBackgroundTask.csproj" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\MyBackgroundTask\MyBackgroundTask.csproj" />
</ItemGroup>
</Project><code class="xml"></code></pre><pre class="code prettyprint"><code class="xml"><Package>
<Applications>
<Application>
<Extensions>
<Extension Category="windows.backgroundTasks" EntryPoint="MyBackgroundTask.ExampleBackgroundTask">
<BackgroundTasks>
<Task Type="systemEvent"/>
</BackgroundTasks>
</Extension>
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package></code></pre></li>
</ul>上面步驟完成就可以讓 WPF 程式呼叫 Background Task 與觸發 Windows runtime component 了。<br />
另外,可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-supported-api" target="_blank">UWP APIs available to a packaged desktop app</a> 瞭解那些 APIs 是 packaged application 可以使用的。<br />
<br />
[範例程式] <br />
<ul><li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/29-WPFAndUWPSample" target="_blank">29-WPFAndUWPSample</a></li>
<li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/32-UWPWithWin32" target="_blank">32-UWPWithWin32</a></li>
<li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/33-WPFWithBackgroundTask" target="_blank">33-WPFWithBackgroundTask</a></li>
</ul>======<br />
如果您的 WPF 程式不想上 Store 只是想要用到 Windows 10 APIs,可以參考 <a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/" target="_blank">Calling Windows 10 APIs From a Desktop Application</a> 介紹的 <a href="https://blogs.windows.com/buildingapps/2017/01/17/announcing-uwpdesktop-nuget-package-version-14393/#Oehg5mUf06dRchpL.97" target="_blank">UWPDesktop package</a> 方便您使用 APIs。<br />
另外需要注意的是 2019 開始 Windows 10 支援 ARM64,目前不確定 Desktop Bridge 上去的 App 是否還有其他需要調整的地方,我也會研究再做補充。<br />
<br />
<b>References</b>: <br />
<ul><li><a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/#YEbaaj4xQt8Akg8D.97" target="_blank">Calling Windows 10 APIs From a Desktop Application</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2016/12/19/desktop-bridge-the-migrate-phase-invoking-a-win32-process-from-a-uwp-app/" target="_blank">Desktop Bridge – The Migrate phase: invoking a Win32 process from a UWP app</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-enhance" targrt="_blank">Enhance your desktop application for Windows 10</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/tag/desktop-bridge/" targe="_blank">App Consult Team - Desktop Bridge</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2018/10/31/add-push-notifications-the-easy-way-with-partner-center-microsoft-store-services-sdk/" target="_blank">Add Push Notifications the easy way with Partner Center + Microsoft Store Services SDK</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extend" target="_blank">Extend your desktop application with modern UWP components</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop" target="_blank">Send a local toast notification from desktop C# apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-packaging-dot-net" target="_blank">Package a desktop application by using Visual Studio</a></li>
<li><a href="https://github.com/Microsoft/DesktopBridgeToUWP-Samples/tree/master/Samples/VB6withXaml" target="_blank">Microsoft/DesktopBridgeToUWP-Samples</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/02/01/adding-uwp-features-existing-pc-software/#IWBsyxuS7edlSyiC.97" target="_blank">o your existing PC software</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/desktop/apiindex/uwp-apis-callable-from-a-classic-desktop-app" target="_blank">UWP APIs callable from a classic desktop app</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/" target="_blank">Calling Windows 10 APIs From a Desktop Application</a></li>
<li><a href="https://code.msdn.microsoft.com/windowsdesktop/sending-toast-notifications-71e230a2" target="_blank">Sending toast notifications from desktop apps sample</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com3tag:blogger.com,1999:blog-2649688415868412622.post-30041117090672609162019-01-25T22:03:00.000+08:002019-01-28T18:17:39.265+08:00XAMLHost 讓 WinForm / WPF 也能使用 Modem UI//Build 2018 提出 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls" target="_blank">XAML Islands</a> 幫助 WPF/WinForm 應用程式使用 UWP 的 XAML controls,讓既有的應用程式可以在不同 Windows 10 設備有更好的體驗(例如:<a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/input/pen-and-stylus-interactions" target="_blank">Windows Ink</a> 或 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/fluent-design-system/index" target="_blank">Fluent Design</a>)。本篇介紹基本導入與使用時遇到的問題。<br />
<a name='more'></a><br />
根據 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls" target="_blank">UWP controls in desktop applications</a> 與 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/" target="_blank">Windows Community Toolkit Documentation</a> 的介紹,WPF 加入 UWP 控制項有幾個做法:<br />
<ul><li>利用 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls#wrapped-controls" target="_blank">Wrapped controls</a>:<div><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls#wrapped-controls" target="_blank">Wrapped controls</a> 由 <a href="https://docs.microsoft.com/windows/uwpcommunitytoolkit/" target="_blank"> Windows Community Toolkit</a> 提供,包裝幾個常用的類型:<a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/webview" target="_blank">WebView</a>, <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/webviewcompatible" target="_blank">WebViewCompatible</a>, <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/inkcanvas" target="_blank">InkCanvs</a> / <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/inktoolbar" target="_blank">InkToolbar</a>, <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/mediaplayerelement" target="_blank">MediaPlayerElmenet</a> 與 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/mapcontrol" target="_blank">MapControl</a>。需要注意:<b>不同的 Controls 支援的 OS 版本不同</b>;<br />
以 WebView 的使用方式爲例,如下:<br />
<ol type="1"><li>安裝 <a href="https://www.nuget.org/packages/Microsoft.Toolkit.Wpf.UI.Controls.WebView" target="_blank">Microsoft.Toolkit.Wpf.UI.Controls.WebView</a>;</li>
<li>為 WPF 加入 application manifest file (link),並設定下面的參數:<pre class="code prettyprint"><code class="xml"><compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect :
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
</windowsSettings>
</application></code></pre></li>
<li>最後在畫面中加入就可以使用了:<pre class="code prettyprint"><code class="xml"><Window xmlns:local="clr-namespace:WpfWrappedControls"
xmlns:toolkit="clr-namespace:Microsoft.Toolkit.Wpf.UI.Controls;assembly=Microsoft.Toolkit.Wpf.UI.Controls.WebView"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<toolkit:WebView Source="http://www.dotblogs.com.tw/pou" />
</Grid>
</Window></code></pre></li>
</ol></div></li>
<li>利用 <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost" target="_blank">WindowsXamlHost</a>:<div>XAMLHost 能代理 <a href="https://docs.microsoft.com/uwp/api/windows.ui.xaml.uielement" target="_blank">Windows.UI.Xaml.UIElement</a> 所有 controls,但至少需要 Windows 10 1809(17763) 以上。<br />
因此,在使用 <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost" target="_blank">WindowsXamlHost</a> 需要先為 WPF 專案加入 <span class="inline-code">C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Windows.winmd</span> 參考。<br />
參考架構圖:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls#architecture-overview" imageanchor="1" ><img border="0" src="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/images/host-controls.png" data-original-width="800" data-original-height="303" /></a><br />
可得知 XAML Host API 與 WebView Win32 API 隨著 OS 已經推出,但是任然有些限制:<a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls?WT.mc_id=ondotnet-channel9-cephilli#limitations" target="_blank">Limitations</a>。<br />
往下介紹 <a href="https://docs.microsoft.com/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost" target="_blank">WindowsXamlHost</a> 的使用:<br />
<ol type="1"><li>安裝 <a href="https://www.nuget.org/packages/Microsoft.Toolkit.Wpf.UI.XamlHost" target="_blank">Microsoft.Toolkit.Wpf.UI.XamlHost</a> 並設定 .NET Framework 4.6.2 以上;如果您的專案是 WinForms 可以參考 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost" target="_blank">Get started</a> 的步驟;</li>
<li>在 WPF 的 XAML 中加入 XamlHost 包裝的控制項目,並設定 <span class="inline-code">InitialTypeName</span> 為何種 Control 類型與 <span class="inline-code">ChildChanged</span> 事件來加入 XamlHost 實際要顯示的内容與對應注冊的事件:<div><pre class="code prettyprint"><code class="xml"><Window x:Class="WpfAppHost.MainWindow"
xmlns:xamlHost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost">
<Grid>
<xamlHost:WindowsXamlHost InitialTypeName="Windows.UI.Xaml.Controls.Button" ChildChanged="WindowsXamlHost_ChildChanged"/>
</Grid>
</Window></code></pre><pre class="code prettyprint"><code class="csharp">private void WindowsXamlHost_ChildChanged(object sender, EventArgs e)
{
WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;
// 利用 Windows.UI.Xaml.Controls 做為轉換
Windows.UI.Xaml.Controls.Button button = (Windows.UI.Xaml.Controls.Button)windowsXamlHost.Child;
Windows.UI.Xaml.Controls.TextBlock txt = new Windows.UI.Xaml.Controls.TextBlock();
txt.Text = "Click me";
button.Content = txt;
}</code></pre>在 <span class="inline-code">ChildChanged</span> 中建立 UI 需要的 Controls,但改用 <a href="https://docs.microsoft.com/uwp/api/windows.ui.xaml.uielement" target="_blank">Windows.UI.Xaml.UIElement</a> 的内容來組合。這個跟 WPF 使用的 <span class="inline-code">System.Windows.Controls</span> 完全不同。<br />
另外,如果您在編寫時 <a href="https://docs.microsoft.com/uwp/api/windows.ui.xaml.uielement" target="_blank">Windows.UI.Xaml.UIElement</a> 編譯失敗,請檢查是否匯入 <span class="inline-code">C:\Program Files (x86)\Windows Kits\10\UnionMetadata\10.0.17763.0\Windows.winmd</span> 參考了。</div></li>
<li>搭配自定義的 UWP Controls 到 WPF 專案中:<div>除了上述介紹需要在 <span class="inline-code">ChildChanged</span> 事件中 code-behind 的逐一加入 UWP Controls 外,XamlHost 還提供直接匯入您在 UWP 建立好的 custom controls。<br />
<ol type="i"><li>準備一個 UWP Class Library 的專案,裏面放置您要導入 WPF 的 UWP custom controls,例如:範例的專案名稱:MyUWPControls;</li>
<li>編輯 MyUWPControls 的專案檔案 (*.csproj):<div><pre class="code prettyprint"><code class="xml"><!-- 要加在 Microsoft.Windows.UI.Xaml.CSharp.targets 之前 -->
<PropertyGroup>
<EnableTypeInfoReflection>false</EnableTypeInfoReflection>
<EnableXBindDiagnostics>false</EnableXBindDiagnostics>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\WindowsXaml\v$(VisualStudioVersion)\Microsoft.Windows.UI.Xaml.CSharp.targets" />
<!-- 要加在 Microsoft.Windows.UI.Xaml.CSharp.targets 之後 -->
<PropertyGroup>
<!-- WpfAppHost 是我的 WPF 專案名稱,它代表是 UWP 專案輸出的整合對象 -->
<HostFrameworkProject>WpfAppHost</HostFrameworkProject>
</PropertyGroup>
<PropertyGroup>
<!-- Copy source and build output files to hostapp folders -->
<!-- Default Winforms/WPF projects do not use $Platform for build output folder -->
<PostBuildEvent>
xcopy "$(TargetDir)*.xbf" "$(SolutionDir)$(HostFrameworkProject)\bin\$(Configuration)\$(ProjectName)\" /Y
xcopy "$(ProjectDir)*.xaml" "$(SolutionDir)$(HostFrameworkProject)\bin\$(Configuration)\$(ProjectName)\" /Y
xcopy "$(ProjectDir)*.xaml.cs" "$(SolutionDir)$(HostFrameworkProject)\$(ProjectName)\" /Y
xcopy "$(ProjectDir)$(IntermediateOutputPath)*.g.*" "$(SolutionDir)$(HostFrameworkProject)\$(ProjectName)\" /Y
</PostBuildEvent>
</PropertyGroup></code></pre>上面的參數是讓 UWP 專案在建置時,把 xbf, xaml, xaml.cs 複製到 host app 的目錄,也就是 WPF 專案裏。<br />
此時,您到 WPF 專案會發現多了一個跟 UWP 專案的目錄名稱,請把它加入 WPF 的專案裏面,如下圖:<br />
<a href="https://2.bp.blogspot.com/-7dGAFOiOHSU/XEsMIerSdVI/AAAAAAAAA9E/JqzRVfO6n4IVNGSrQkm5GwWVG-1K6qAeQCLcBGAs/s1600/2.png" imageanchor="1" ><img border="0" src="https://2.bp.blogspot.com/-7dGAFOiOHSU/XEsMIerSdVI/AAAAAAAAA9E/JqzRVfO6n4IVNGSrQkm5GwWVG-1K6qAeQCLcBGAs/s320/2.png" width="249" height="320" data-original-width="256" data-original-height="329" /></a><br />
可發現 UWP 專案中的 BlankPage1.xaml 被編程多個 <span class="inline-code">*.g.cs</span> 與 <span class="inline-code">*.xaml.cs</span>,如果您開發 UWP 其實就會發現 XAML 編譯好的内容在建置本來就有這些,這些檔案記錄的是 XAML 配置的内容與參數。<br />
WPF 專案加入這些參考後,<a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost#get-started" target="_blank">XamlHost</a> 再使用時就會被匯入。 <br />
BlankPage1.xaml 是我加入的内容:<br />
<pre class="code prettyprint"><code class="xml"><Page>
<Grid>
<TextBlock Text="{x:Bind WPFMessage}" FontSize="50"></TextBlock>
</Grid>
</Page></code></pre><pre class="code prettyprint"><code class="csharp">public sealed partial class BlankPage1 : Page
{
// 作爲 x:bind 的來源,如果您本身使用 ViewModel 要記得開放入口讓外部可以使用
public string WPFMessage { get; set; }
public BlankPage1()
{
this.InitializeComponent();
}
}</code></pre></div></li>
<li>匯入之後在 WPF 專案要怎麽使用:<div><pre class="code prettyprint"><code class="xml"><Window x:Class="WpfAppHost.MainWindow"
xmlns:xamlHost="clr-namespace:Microsoft.Toolkit.Wpf.UI.XamlHost;assembly=Microsoft.Toolkit.Wpf.UI.XamlHost">
<Grid>
<!-- 利用 WindowsXamlHost 包裝 UWP 專案的内容 -->
<xamlHost:WindowsXamlHost InitialTypeName="MyUWPControls.BlankPage1" ChildChanged="MyUWPPage_ChildChanged" />
</Grid>
</Window></code></pre><span class="inline-code">InitialTypeName="MyUWPControls.BlankPage1"</span> 放入的是 UWP 專案中的 namespace,再搭配 <span class="inline-code">ChildChanged</span> 事件來調整顯示的内容。<br />
<pre class="code prettyprint"><code class="csharp">private void MyUWPPage_ChildChanged(object sender, EventArgs e)
{
// 利用 GetUwpInternalObject() 把 UWP 的内容截取出來,並轉型成對應的控制項
WindowsXamlHost windowsXamlHost = (WindowsXamlHost)sender;
global::MyUWPControls.BlankPage1 myUWPPage = windowsXamlHost.GetUwpInternalObject() as global::MyUWPControls.BlankPage1;
if (myUWPPage != null)
{
myUWPPage.WPFMessage = this.WPFMessage;
}
}</code></pre>改用這種方式會讓控制項目整合更加方便,雖然發現 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost#get-started" target="_blank">XamlHost</a> 相對容易很多,大部分的 Controls 都能相容,不過比較複雜的自定義控制項目或是比較新的控制項目就不一定會支援,建議參考 <a href="" target="_blank">Limit</a> 做比對。<br />
</div></li>
</ol></div></li>
</ol></div></li>
</ul><br />
<b>補充</b>:<br />
<ul><li>讓 WinForm 程式支援高 DPI 環境可以參考 <a href="https://docs.microsoft.com/en-us/dotnet/framework/winforms/high-dpi-support-in-windows-forms#configuring-your-windows-forms-app-for-high-dpi-support" target="_blank">Configuring your Windows Forms app for high DPI support</a> 裏面的設定</li>
<li>如果是 C++ 的專案要加入 UWP XAML Control 可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/using-the-xaml-hosting-api" target="_blank"> Using the XAML hosting API in a desktop application</a> 説明</li>
</ul><br />
[範例程式]<br />
<a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/31-WPFAndXamlHostSample/WpfAppHost">31-WPFAndXamlHostSample</a><br />
======<br />
對於微軟在 Windows Apps 的發展,可發現它爲了讓既有的應用程式可以更好地運作在 Windows 10 的所有設備下不少苦心。<br />
如果您的公司或是自己的專案是用 WPF 開發希望可以納入更多 UWP 的效果,真的可以考慮使用 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost#get-started" target="_blank">XamlHost</a>。<br />
更可以期待的是 <a href="https://docs.microsoft.com/zh-tw/dotnet/core/whats-new/dotnet-core-3-0#windows-desktop" target="_blank">.NET Core 3.0</a> 更支援 Window Forms 應用程式的編程。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://channel9.msdn.com/Shows/On-NET/Integrating-UWP-components-into-Win32-applications" target="_blank">Integrating UWP components into Win32 applications</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2018/11/02/xaml-islands-a-deep-dive-part-1/" target="_blank">XAML Islands – A deep dive – Part 1</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls" target="_blank">UWP controls in desktop applications</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/controls/wpf-winforms/windowsxamlhost" target="_blank">WindowsXamlHost control for Windows Forms and WPF</a></li>
<li><a href="https://www.telerik.com/blogs/getting-started-xaml-islands-hosting-uwp-control-in-wpf-winforms-apps" target="_blank">Getting Started with XAML Islands: Hosting a UWP Control in WPF and WinForms Apps</a></li>
<li><a href="https://csharp.christiannagel.com/2018/11/06/xamlisland/" target="_blank">Desktop Applications with XAML. Part 3: XAML Islands</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls?WT.mc_id=ondotnet-channel9-cephilli#uwp-xaml-hosting-api" target="_blank">UWP XAML hosting API</a></li>
<li><a href="https://msdn.microsoft.com/en-us/magazine/mt848632/" target="_blank">What's New in Visual Studio 2019</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/fluent-design-system/index" target="_blank">The Fluent Design System for Windows app creators</a></li>
<li><a href="https://www.infoq.com/news/2018/08/Modern-Desktop-Applications" target="_blank">Modernizing Windows Desktop Applications with XAML Islands</a></li>
<li><a href="https://www.infoq.cn/article/2018%2F08%2FModern-Desktop-Applications" target="_blank">通过 XAML Islands 使 Windows 桌面应用程序现代化</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-89925289469286546302018-12-31T18:26:00.000+08:002019-02-26T10:27:48.674+08:00WPF 使用 Windows 10 APIs - 1<a href="https://developer.microsoft.com/en-us/events/build/content/announcing-uwp-xaml-islands?playlist=80d147e8-f3b0-4ca0-a96f-cfc8e80bec20" target="_blank">//build 2018</a> 看到微軟對於 Win32 程式 (WinForms/WPF) 增加新的 SDKs,讓我想起之前做 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-root" target="_blank">Desktop Bridge</a> 的心得。<br />
利用這篇介紹幾個例子,幫助大家熟悉怎麽在 WPF 使用 Windows 10 APIs。<br />
<a name='more'></a><br />
<b>Desktop Bridge 有三個大方向</b>:<br />
<ol><li>把既有的 Win32 installer 利用 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-run-desktop-app-converter" target="_blank">Desktop App Convert tool</a> 轉成 AppX Package;</li>
<li>讓既有的 Win32 application 支援 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-supported-api" target="_blank">UWP APIs</a>,例如:toast notification, update live tiles 等;</li>
<li>讓既有的 Win32 application 使用 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extend#create-a-background-task" target="_blank">UWP components</a>,例如:background task;</li>
</ol>[<b>重要</b>]<br />
根據 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-enhance" target="_blank">Enhance your desktop application for Windows 10</a> 介紹,在 WPF 專案中加入以下的參考才能使用 Windows 10 APIs:<div><a href="https://1.bp.blogspot.com/-uw_Q5chHo3I/XBXnSVRsOiI/AAAAAAAAA7A/FIFJZmt54pUMFxQDcnxh-YUxfD6oxMx7gCLcBGAs/s1600/refe.png" imageanchor="1" ><img border="0" src="https://1.bp.blogspot.com/-uw_Q5chHo3I/XBXnSVRsOiI/AAAAAAAAA7A/FIFJZmt54pUMFxQDcnxh-YUxfD6oxMx7gCLcBGAs/s640/refe.png" width="640" height="256" data-original-width="747" data-original-height="299" /></a></div>這些加入的參考,要記得把 <span class="inline-code">Local Copy = false</span>。<br />
[<b>注意</b>] <br />
如果遇到 Windows 相關 reference 找不到,直接加入<br />
<span class="inline-code">C:\Program Files (x86)\Windows Kits\10\UnionMetadata\Windows.winmd</span> 與 <br />
<span class="inline-code">C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETCore\v4.5\*WindowsRuntime*.dll</span>;<br />
或者利用從 <a href="https://www.nuget.org/packages/UwpDesktop/10.0.14393.3" target="_blank">UwpDesktop</a> 加入 Nuget。<br />
如果是 C++ 專案可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-enhance#modify-a-c-project-to-use-windows-runtime-apis" target="_blank">Modify a Windows Desktop application project to add C++/WinRT support</a> 的設定。<br />
<br />
可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-supported-api" target="_blank">UWP APIs available to a packaged desktop app</a> 知道有那些 APIs 可以在 Desktop 下使用,也需注意 API contract 的限制,不同的 contract 會相依于加入參考使用的 Windows 10 版本。<br />
如果遇到一些 API 使用時沒有反應,代表它只能用在擁有 <span class="inline-code">Package identity information</span>,此時就需要 WPF 包裝成 Packages。<br />
<b>封裝方式</b>:<br />
<ol><li>建立 Windows Application Packaging Project;<div><a href="https://4.bp.blogspot.com/-6q-vpo7-5xw/XB2vxVXd3UI/AAAAAAAAA7Y/Rlf4tXiZ7sEiXs6kjaUDXyDdz46JwPE_QCLcBGAs/s1600/1.png" imageanchor="1" ><img border="0" src="https://4.bp.blogspot.com/-6q-vpo7-5xw/XB2vxVXd3UI/AAAAAAAAA7Y/Rlf4tXiZ7sEiXs6kjaUDXyDdz46JwPE_QCLcBGAs/s640/1.png" width="640" height="444" data-original-width="941" data-original-height="653" /></a></div></li>
<li>在 Packaging Project 加入要包裝的 WPF 專案;<div><a href="https://3.bp.blogspot.com/-SntmPHLYjpA/XCnxRNB_3ZI/AAAAAAAAA78/q44HX2QGtDkgB_GmWMkvSiSWGzPrsPsHgCLcBGAs/s1600/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png" imageanchor="1" ><img border="0" src="https://3.bp.blogspot.com/-SntmPHLYjpA/XCnxRNB_3ZI/AAAAAAAAA78/q44HX2QGtDkgB_GmWMkvSiSWGzPrsPsHgCLcBGAs/s320/%25E6%259C%25AA%25E5%2591%25BD%25E5%2590%258D.png" width="320" height="209" data-original-width="678" data-original-height="442" /></a></div></li>
</ol>加入參考之後,舉幾個列子説明如何使用:<br />
<ul><li><b>抓取坐標資訊</b>:<pre class="code printpretty"><code class="csharp">var locator = new Windows.Devices.Geolocation.Geolocator();
var location = await locator.GetGeopositionAsync();
var position = location.Coordinate.Point.Position;
var latlong = string.Format("lat:{0}, long:{1}", position.Latitude, position.Longitude);
var result = MessageBox.Show(latlong);</code></pre>可以參考 <a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/" target="_blank">How to access the Windows 10 APIs from WPF</a>。</li>
<li><b>發送 Toast</b>:<div><pre class="code prettyprint"><code class="csharp">private async void OnSendToastClick(object sender, RoutedEventArgs e)
{
string title = "featured picture of the day";
string content = "beautiful scenery";
string image = "https://picsum.photos/360/180?image=104";
string logo = "https://picsum.photos/64?image=883";
string xmlString = $@"<toast><visual>
<binding template='ToastGeneric'>
<text>{title}</text>
<text>{content}</text>
<image src='{image}'/>
<image src='{logo}' placement='appLogoOverride' hint-crop='circle'/>
</binding>
</visual></toast>";
XmlDocument toastXml = new XmlDocument();
toastXml.LoadXml(xmlString);
ToastNotification toast = new ToastNotification(toastXml);
ToastNotificationManager.CreateToastNotifier().Show(toast);
}</code></pre>由於寄送 Toast/Tile 需要 <span class="inline-code">identify</span>,所以要記得建立 Windows Application Packaging Project 搭配使用。<br />
如何處理用戶點擊的 Toast 呢?參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop" target="_blank">Send a local toast notification from desktop C# apps</a> 來説明。<br />
</div></li>
<li><b>建立 Tile</b>:<div>根據 <a href="https://blogs.msdn.microsoft.com/universal-windows-app-model/2017/05/25/create-secondary-tiles-from-your-desktop-application/" target="_blank">Create Secondary Tiles from your Desktop Application</a> 介紹,需要利用 COM 的介面操作 Tile 的建立。如下:<br />
<pre class="code prettyprint"><code class="csharp">// This interface definition is necessary because this is a non-universal
// app and we have transfer the hwnd for the window to the WinRT object.
[ComImport]
[Guid("3E68D4BD-7135-4D10-8018-9FB6D9F33FA1")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IInitializeWithWindow
{
void Initialize(IntPtr hwnd);
}
private async Task PinToStart()
{
// Initialize the tile with required arguments
SecondaryTile tile = new SecondaryTile("myTileId5391", "Display name", "myActivationArgs", new Uri("ms-appx:///Images/Square150x150Logo.png"), TileSize.Default);
// Assign the window handle
IInitializeWithWindow initWindow = (IInitializeWithWindow)(object)tile;
initWindow.Initialize(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
// Pin the tile
bool isPinned = await tile.RequestCreateAsync();
}
</code></pre></div></li>
<li><b>操作 SMTC</b>:<div>要操作 SMTC 需要改用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.media.playback.mediaplayer" target="_blank">MediaPlayer</a>,並設定 <span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.Media.Playback.MediaPlaybackCommandManager" target="_blank">MediaPlayer.CommandManager</a></span> 來注冊按鈕事件與狀態。<br />
<pre class="code prettyprint"><code class="csharp">// 建立 MediaPlayer 並注冊 MediaCommonManager 事件
Player = new MediaPlayer();
Player.CommandManager.IsEnabled = true;
Player.CommandManager.PauseReceived += CommandManager_PauseReceived;
Player.CommandManager.PlayReceived += CommandManager_PlayReceived;
Player.CommandManager.NextReceived += CommandManager_NextReceived;
Player.CommandManager.PreviousReceived += CommandManager_PreviousReceived;
// 建立 MediaPlaybackList 利用每一個 Item 設定 MediaItemDisplayProperties 來更新 SMTC
MediaPlaybackList = new MediaPlaybackList();
for (int i = 1; i < 5; i++)
{
MediaSource source = MediaSource.CreateFromUri(new Uri($"{Package.Current.InstalledLocation.Path}/WPFAndUWPSample/Assets/mp3/0{i}.mp3", UriKind.RelativeOrAbsolute));
MediaPlaybackItem item = new MediaPlaybackItem(source);
// 設定為 Music 的相關屬性
MediaItemDisplayProperties displayProperty = item.GetDisplayProperties();
displayProperty.Type = MediaPlaybackType.Music;
displayProperty.MusicProperties.Title = $"0{i}.mp3";
displayProperty.MusicProperties.AlbumArtist = "JJ";
displayProperty.Thumbnail = RandomAccessStreamReference.CreateFromUri(new Uri($"{Package.Current.InstalledLocation.Path}/WPFAndUWPSample/Assets/mp3/0{i}.jpg")); ;
item.ApplyDisplayProperties(displayProperty);
MediaPlaybackList.Items.Add(item);
}
Player.PlaybackList = MediaPlaybackList;
Player.Play();
</code></pre>更多關於 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.media.playback.mediaplayer" target="_blank">MediaPlayer</a> 的操作可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols" target="_blank">Integrate with the System Media Transport Controls</a>。<br />
如果您專案使用別的 Player 也希望能操作 SMTC 的話,可改用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.media.playback.backgroundmediaplayer" target="_blank">BackgroundMediaPlayer</a> 的方式來更新 SMTC 與注冊按鈕事件。<pre class="code prettyprint"><code class="csharp">private void UseBackgroundMediaPlayer()
{
BackgroundMediaPlayer.Current.SystemMediaTransportControls.ButtonPressed += Smtc_ButtonPressed;
var updater = BackgroundMediaPlayer.Current.SystemMediaTransportControls.DisplayUpdater;
updater.MusicProperties.Title = "song name";
updater.MusicProperties.AlbumArtist = "artsit and album";
updater.Update();
}
private void Smtc_ButtonPressed(SystemMediaTransportControls sender, SystemMediaTransportControlsButtonPressedEventArgs args)
{
// 處理在 SMTC 操作的事件
}</code></pre></div></li>
</ul>上面介紹 Win32 程式怎麽使用 UWP APIs,接著參考 <a href="https://blogs.msdn.microsoft.com/appconsult/2018/07/29/packaging-a-uwp-application-with-a-win32-component-in-the-right-way/" target="_blank">Packaging a UWP application with a Win32 component in the right way</a> 與 <a href="https://blogs.msdn.microsoft.com/appconsult/2016/12/19/desktop-bridge-the-migrate-phase-invoking-a-win32-process-from-a-uwp-app/" target="_blank">Desktop Bridge – The Migrate phase: invoking a Win32 process from a UWP app</a> 説明,補充 UWP app 怎麽與 Win32 程式互動。<br />
UWP app 與 Win32 App 的互動中重點:<br />
<ol><li>建立一個 Windows Application Packaging Project 並把 UWP app 與 Win32 App 加入 Applicaions 集合,設定 UWP app 為起始專案;</li>
<li>在 Package.appxmanifest 注冊: <b>windows.appService</b> 與 <b>windows.fullTrustProces</b>:<div><pre class="code prettyprint"><code class="xml"><Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="$targetentrypoint$">
<uap:VisualElements />
<Extensions>
<uap:Extension Category="windows.appService">
<uap:AppService Name="com.pou.MyAppService" />
</uap:Extension>
<desktop:Extension Category="windows.fullTrustProcess" Executable="WPFApp\WPFApp.exe" />
</Extensions>
</Application>
</Applications>
<Capabilities>
<Capability Name="internetClient" />
<rescap:Capability Name="runFullTrust" />
</Capabilities></code></pre></div></li>
<li>UWP app 建立處理 AppService 的邏輯,並爲了呼叫 FullTrustProcess 需要為 UWP app 加入 <b>Windows Desktop Extensions for the UWP</b>;<div>由於 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">App Service</a> 在 UWP app 安裝時會一并被安裝到系統裏面,它與 BackgroundTask 不一樣需依賴 Trigger 的機制,而是讓呼叫端利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.appservice.appserviceconnection.packagefamilyname" target="_blank">PackageFamilyName</a> 與 <a href="https://docs.microsoft.com/zh-tw/uwp/api/windows.applicationmodel.appservice.appserviceconnection.appservicename#Windows_ApplicationModel_AppService_AppServiceConnection_AppServiceName" target="_blank">AppServiceName</a> 來指定啓動它。因此,它很適合扮演傳遞的角色;更多關於 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">App Service</a> 的介紹可參考 <a href="https://dotblogs.com.tw/pou/2018/07/14/003047" target="_blank">UWP - 介紹 App Service 與新功能</a>。<pre class="code prettyprint"><code class="csharp">private AppServiceConnection appServiceConnection;
private BackgroundTaskDeferral appServiceDeferral;
/// AppService 在 Win10 Anniversary Update (1607) 開始支援 Sinle Process 的做法,利用 OnBackgroundActivated 來處理 AppService 的請求。
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
AppServiceTriggerDetails appService = args.TaskInstance.TriggerDetails as AppServiceTriggerDetails;
// appServiceDeferral 與 appServiceConnection 需要變成公用變數
// 因爲其他時間需要用到,已維持連線的一致性
appServiceDeferral = args.TaskInstance.GetDeferral();
appServiceConnection = appService.AppServiceConnection;
appServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
}
private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
// 當 App Service 收到請求時,該 method 就會被觸發
// 先要求取得 取得 deferral 拉長生命周期
var requestDeferral = args.GetDeferral();
ValueSet message = args.Request.Message;
// 抓到從 Win32 App 送來的内容,顯示在 MainPage.xaml
string name = message["name"] as string;
if (string.IsNullOrEmpty(name) == false && Window.Current != null)
{
var rootFrame = Window.Current.Content as Frame;
if (rootFrame != null && rootFrame.Content != null)
{
var mainPage = rootFrame.Content as MainPage;
mainPage.SetResponse(name);
}
}
// 建立回傳給 Win32 的訊息
ValueSet responseMsg = new ValueSet();
responseMsg.Add("response", "success");
await args.Request.SendResponseAsync(responseMsg);
requestDeferral.Complete();
}</code></pre>那麽,要怎麽從 UWP app 呼叫 Win32 app 呢?如下:<pre class="code prettyprint"><code class="csharp">private async void OnInvokeWin32AppClick(object sender, RoutedEventArgs e)
{
// 如需要傳遞參數到 Win32 app,FullTrustProcessLauncher 也有支援,不過要在 Package.appxmanifest 加入宣告
await Windows.ApplicationModel.FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
}</code></pre>更多詳細内容可以參考:<a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.fulltrustprocesslauncher" target="_blank">FullTrustProcessLauncher</a>。</div></li>
<li>Win32 app 利用 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">AppService</a> 利用 將參數送到 UWP app;<div>如果 UWP app 與 Win32 app 被封裝在同一個 Pakcage 裏面,在 Win32 app 使用 AppServiceConnection 時可用 <span class="inine-code">Windows.ApplicationModel.Package.Current.Id.FamilyName</span> 來抓取 PackageFamilyName。<pre class="code prettyprint"><code class="csharp">private async void OnSendToUWPClick(object sender, RoutedEventArgs e)
{
// 利用 Windows.ApplicationModel 的模組,要記得為 Win32 app 加入 Windows.winmd 與 Windows.Runtime 的參考
AppServiceConnection connection = new AppServiceConnection();
connection.AppServiceName = "com.pou.MyAppService";
connection.PackageFamilyName = Windows.ApplicationModel.Package.Current.Id.FamilyName;
var result = await connection.OpenAsync();
if (result == AppServiceConnectionStatus.Success)
{
ValueSet valueSet = new ValueSet();
valueSet.Add("name", txtUserName.Text);
var response = await connection.SendMessageAsync(valueSet);
if (response.Status == AppServiceResponseStatus.Success)
{
string responseMessage = response.Message["response"].ToString();
if (responseMessage == "success")
{
this.Hide();
}
}
}
}</code></pre></div></li>
</ol>[補充]<br />
<ul><li><a href="https://docs.microsoft.com/en-us/uwp/extension-sdks/windows-desktop-extension-sdk" target="_blank">UWP Desktop Extensions</a> 裏面有一些相關的 APIs 可幫助 UWP 更多能力操作 Desktop 的功能;</li>
<li>Desktop Bridge 讓 desktop application 有了 <span class="inline-code">identify</span>,藉由它就能存取 UWP APIs。有關準備怎麽封裝 desktop application 可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-prepare" target="_blank">Prepare to package a desktop application</a>;</li>
</ul>[範例程式] <br />
<ul><li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/29-WPFAndUWPSample" target="_blank">29-WPFAndUWPSample</a></li>
<li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/32-UWPWithWin32" target="_blank">32-UWPWithWin32</a></li>
<li><a href="https://github.com/poumason/DotblogsSampleCode/tree/master/Samples/33-WPFWithBackgroundTask" target="_blank">33-WPFWithBackgroundTask</a></li>
</ul>======<br />
本篇目的介紹 WPF 怎麽使用 Windows 10 APIs,以及怎麽與 UWP 之間互動,最後利用 Packaging Project Template 把他們包裝起來。<br />
另外可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-enhance#add-windows-10-experiences" target="_blank">Add Windows 10 experiences</a> 考慮那些功能是真的需要加入到 WPF 專案裏面的。<br />
希望幫忙想要移植現有 WPF 或是 Win32 程式的開發人員能更快評估如何開發。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://blogs.msdn.microsoft.com/appconsult/tag/desktop-bridge/" target="_blank">App Consult Team - Desktop Bridge</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/12/04/extend-desktop-application-windows-10-features-using-new-visual-studio-application-packaging-project/#QlUBxtod0TzbiqwD.97" target="_blank">Extend your desktop application with Windows 10 features using the new Visual Studio Application Packaging Project</a></li>
<li><a href="https://github.com/Microsoft/DesktopBridgeToUWP-Samples/tree/master/Samples/WinFormsUpdateTaskSample" target="_blank">DesktopBridgeToUWP-Samples</a><br />
</li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-root#benefits" target="_blank">Package desktop applications (Desktop Bridge)</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/02/01/adding-uwp-features-existing-pc-software/#IWBsyxuS7edlSyiC.97" target="_blank">Adding UWP Features to your Existing PC Software,” which goes into even more detail on the topic.</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/" target="_blank">Calling Windows 10 APIs From a Desktop Application</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2016/12/19/desktop-bridge-the-migrate-phase-invoking-a-win32-process-from-a-uwp-app/" target="_blank">Desktop Bridge – The Migrate phase: invoking a Win32 process from a UWP app</a> (重要)</li>
<li><a href="https://github.com/Microsoft/DesktopBridgeToUWP-Samples/tree/master/Samples/DesktopAppTransition" target="_blank">WPF picture viewer with transition/migration/uninstallation</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-host-controls" targetr="_blank">Host UWP controls in WPF and Windows Forms applications</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/integrate-with-systemmediatransportcontrols" target="_blank">Integrate with the System Media Transport Controls</a></li>
<li><a href="https://stefanwick.com/2018/05/15/global-hotkey-registration-in-uwp/" target="_blank">Global hotkey registration in UWP</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/packaging/packaging-uwp-apps" target="_blank">Package a UWP app with Visual Studio</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/porting/desktop-to-uwp-extend" target="_blank">Extend your desktop application with modern UWP components</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/shell/tiles-and-notifications/secondary-tiles-desktop-pinning" target="_blank">從傳統型應用程式釘選次要磚</a></li>
<li><a href="https://msdn.microsoft.com/en-us/library/windows/desktop/mt695951(v=vs.85).aspx" target="_blank">UWP APIs callable from desktop applications</a></li>
<li><a href="https://stefanwick.com/2018/04/06/uwp-with-desktop-extension-part-1/" target="_blank">UWP with Desktop Extension – Part 1</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/audio-video-camera/play-audio-and-video-with-mediaplayer" target="_blank">Play audio and video with MediaPlayer</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/desktop/apiindex/uwp-apis-callable-from-a-classic-desktop-app" target="_blank">UWP APIs callable from a classic desktop app</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.Foundation.Metadata.DualApiPartitionAttribute" target="_blank">DualApiPartitionAttribute</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-56564092549547392952018-12-06T13:47:00.000+08:002018-12-06T13:47:19.791+08:00淺談 Uno Platform 開發<a href="https://dotblogs.com.tw/billchung" target="_blank">Bill 叔</a>某天跟我介紹了 <a href="https://platform.uno/" target="_blank">Uno Platform</a> 的好處,讓我想要寫一篇來介紹我移植 UWP app 到 Xamarin 的經驗。<br />
<a name='more'></a><br />
有關於 <a href="https://platform.uno/" target="_blank">Uno Platform</a> 的介紹,建議先閲讀<a href="https://dotblogs.com.tw/billchung" target="_blank">Bill 叔</a>寫好的:<a href="https://dotblogs.com.tw/billchung/series/1?qq=Uno%2520Platform%2520%E5%81%B5%E6%9F%A5%E9%9A%8A" target="_blank">Uno Platform 偵查隊</a> 系列文章。<br />
<br />
先列出開放時遇到的問題:<br />
<ul><li><a href="https://marketplace.visualstudio.com/items?itemName=nventivecorp.uno-platform-addin" target="_blank">Uno Platform Solution Templates</a> 使用最新的 <span class="inline-code">Uno.UI</span>,但問題很多,建議使用 <b>1.41.1-dev.134</b>;</li>
<li>如果遇到 <span class="inline-code">Resource.designer.cs</span> 找不到,有兩個解法:<ol><li>從專案的 <span class="inline-code">obj/Debug/80</span> 中複製到指定的目錄下;</li>
<li><span class="inline-code">Uno.UI</span> 的版本到 <b>1.41.1-dev.134</b>;</li>
</ol></li>
<li>無法處理 Android 鍵盤的 enter (done) 按鍵,所以 KeyDown event 會抓不到。</li>
<li>文章說 <span class="inline-code">x:bind</span> 可以用,但是我測試沒有反應,建議先使用 <span class="inline-code">binding</span></li>
<li><a href="https://github.com/nventive/Uno/blob/master/doc/articles/ListViewBase.md" target="_blank">ListViewBase in Uno</a> 跟 UWP 的差別,整合時需要多測試不相容的問題,例如:Header 不是所有的 XAML UIElement 都能顯示在 Android 或是 iOS 上。</li>
<li>記得修改 <span class="inline-code">AssemblyInfo.cs</span> 的 AssemblyProduct 與 AssemblyTitle 為對的名稱。</li>
</ul><br />
利用 <a href="https://github.com/KKBOX/OpenAPI-Dotnet" target="_blank">KKBOX Open API</a> 製作一個播放器來介紹使用 <a href="https://platform.uno/" target="_blank">Uno Platform</a> 開發有多快速。<br />
使用畫面:<br />
<ul><li>Main Page: 讓用戶輸入關鍵字,藉由 <a href="https://github.com/KKBOX/OpenAPI-Dotnet" target="_blank">KKBOX Open API</a> 找到歌曲;</li>
<li>Youtube Page:根據 Main Page 點擊的歌曲名稱,打 Youtube API 並搭配 WebView 播放内容;</li>
</ul><b>MainPage.xaml.cs 的開發</b>:<br />
<ol><li>由於 <a href="https://github.com/KKBOX/OpenAPI-Dotnet" target="_blank">KKBOX Open API</a> 使用 .NET Standard 開發,所以從 <a href="https://www.nuget.org/packages/KKBOX.OpenAPI.Standard/1.0.1.3" target="_blank">Nuget</a> 為 Android, iOS, UWP 加入參考;</li>
<li>申請 API 並建立 ViewModel 來處理關鍵字搜尋與顯示結果;<pre class="code prettyprint"><code class="language-csharp">public ObservableCollection<trackdatawrapper> Tracks { get; private set; }
private KKBOXAPI apiClient;
public MainPageViewModel()
{
apiClient = new KKBOXAPI();
Tracks = new ObservableCollection<trackdatawrapper>();
}
public async Task InitAPI()
{
// 取得 Access Token
var authResult = await KKBOXOAuth.SignInAsync(clientId, clientSecret);
apiClient.AccessToken = authResult.Content.AccessToken;
}
public async Task SearchAsync()
{
// 搜尋歌曲,並加入 ObservableCollection 顯示在畫面上
var searchResult = await apiClient.SearchAsync(SearchKeyWord, 30, 0, SearchType.track);
Tracks.Clear();
foreach (var item in searchResult.Content.Tracks.Data)
{
// 加入包裝好的 DataWrapper
Tracks.Add(new TrackDataWrapper(item));
}
OnPropertyChanged(nameof(Tracks));
}</code></pre></li>
<li>設計 MainPage.xaml 的顯示 XAML;<pre class="code prettyprint"><code class="xml"><Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox Grid.Column="0" Text="{Binding SearchKeyWord, Mode=TwoWay}"/>
<Button Grid.Column="1" Click="OnSearchButtonClick">
<TextBlock Text="Search" />
</Button>
</Grid>
<ListView Grid.Row="1" ItemsSource="{Binding Tracks, Mode=OneWay}" IsItemClickEnabled="True" ItemClick="OnListViewItemClicked">
<ListView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{Binding AlbumUrl}" Width="60" Height="60" Margin="-10,0,0,0" />
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding SongName}" Margin="10,0" FontSize="16" VerticalAlignment="Center"/>
<TextBlock Text="{Binding ArtistWithAlbumName}" Margin="10,5,0,0" FontSize="14" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ProgressRing Grid.RowSpan="2" IsActive="True" Width="100" Height="100" Visibility="{Binding IsSearching, Converter={StaticResource BoolToVisibilityConverter}}" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid></code></pre></li>
</ol>畫面準備了 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.textbox" target="_blank">TextBox</a> 與 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.button" target="_blank">Button</a>,處理用戶輸入的内容來進行搜尋,並將結果 Binding 回到 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listview" target="_blank">ListView</a>。接著點擊 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listviewitem" target="_blank">ListViewItem</a> 進入 YoutubePage.xaml。<br />
<br />
<b>YoutubePage.xaml.cs 的開發</b>:<br />
<ol><li>利用 <a href="https://github.com/youtube/api-samples/blob/master/dotnet/Search.cs" target="_blank">YouTube Data API: Search</a> 搜尋關鍵字,記得先申請 <a href="https://console.developers.google.com/" target="_blank">API key</a>;</li>
<li>設計 YoutubePage.xaml 的顯示 XAML;<pre class="code prettyprint"><code class="xml"><Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<WebView Grid.Row="0" Source="{Binding PlayingVideoUrl, Mode=OneWay}" Height="{Binding WebPlayerHeight, Mode=OneWay}" Width="{Binding WebPlayerWidth, Mode=OneWay}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch"/>
<ListView Grid.Row="1" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" ItemsSource="{Binding Videos, Mode=OneWay}" IsItemClickEnabled="true" ItemClick="OnListViewItemClicked">
<ListView.ItemTemplate>
<DataTemplate>
<Grid HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Source="{Binding VideoImageUrl}" Width="60" Height="60" Margin="-10,0,0,0" />
<StackPanel Grid.Column="1">
<TextBlock Text="{Binding Name}" Margin="10,0" FontSize="16" VerticalAlignment="Center"/>
</StackPanel>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ProgressRing Grid.RowSpan="2" IsActive="True" Width="100" Height="100"
Visibility="{Binding IsSearching, Converter={StaticResource BoolToVisibilityConverter}}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid></code></pre>藉由 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.webview" target="_blank">WebView</a> 顯示 Youtube 内容,如果您遇到顯示 Youtube 的問題,可以參考 <a href="https://dotblogs.com.tw/pou/2016/03/05/092519" target="_blank">UWP - Add Youtube video in the WebView</a>的介紹;</li>
<li>處理 BackButton 的邏輯:<div>如果熟悉 UWP 可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/basics/navigation-history-and-backwards-navigation" target="_blank">Navigation history and backwards navigation for UWP apps</a>:<br />
<pre class="code prettyprint"><code class="language-csharp">public App()
{
Windows.UI.Core.SystemNavigationManager.GetForCurrentView().BackRequested += App_BackRequested;
}
private void App_BackRequested(object sender, Windows.UI.Core.BackRequestedEventArgs e)
{
Frame rootFrame = Window.Current.Content as Frame;
if (rootFrame.CanGoBack)
{
rootFrame.GoBack();
e.Handled = true;
}
}
</code></pre></div></li>
<li>iOS 沒有實體按鈕可以處理 Back, 則設計 Back 按鈕讓用戶可以回到上一個畫面;</li>
</ol>執行結果:<br />
<ul><li><b>UWP</b><div><a href="https://4.bp.blogspot.com/-OmQ4R5b-apU/XAa79jN4I7I/AAAAAAAAA4Y/knpQ55-BWF0r2y8k9JP8FLCbTFZ9c5jXgCLcBGAs/s1600/uwp_youtube.png" imageanchor="1" ><img border="0" src="https://4.bp.blogspot.com/-OmQ4R5b-apU/XAa79jN4I7I/AAAAAAAAA4Y/knpQ55-BWF0r2y8k9JP8FLCbTFZ9c5jXgCLcBGAs/s400/uwp_youtube.png" width="400" height="297" data-original-width="1600" data-original-height="1189" /></a></div></li>
<li><b>Android</b><div><a href="https://1.bp.blogspot.com/-2DcA4tTrlQM/XAbA39xweeI/AAAAAAAAA4w/1bB4JZB7rnofLe5fDJhthX_P1h2AenyEwCLcBGAs/s1600/android_youtube.png" imageanchor="1" ><img border="0" src="https://1.bp.blogspot.com/-2DcA4tTrlQM/XAbA39xweeI/AAAAAAAAA4w/1bB4JZB7rnofLe5fDJhthX_P1h2AenyEwCLcBGAs/s400/android_youtube.png" width="400" height="343" data-original-width="1600" data-original-height="1371" /></a></div></li>
<li><b>iOS</b><div><a href="https://4.bp.blogspot.com/-gJbTJDcNt8k/XAinHBAxFTI/AAAAAAAAA5Q/Si-MJcoFrQ4djdgJ0Enr8CfHvcxKMraLgCLcBGAs/s1600/ios_youtube.png" imageanchor="1" ><img border="0" src="https://4.bp.blogspot.com/-gJbTJDcNt8k/XAinHBAxFTI/AAAAAAAAA5Q/Si-MJcoFrQ4djdgJ0Enr8CfHvcxKMraLgCLcBGAs/s400/ios_youtube.png" width="400" height="377" data-original-width="1600" data-original-height="1506" /></a><br />
</div></li>
</ul>從上面的介紹裏面,是否發現一個重點:<b>所有的 code 跟 xaml 都寫在 Shared 的專案裏面</b>。<br />
但是不同的平臺内容,依舊需在平臺專案下建立或是利用 <a href="https://docs.microsoft.com/en-us/xamarin/cross-platform/app-fundamentals/building-cross-platform-applications/platform-divergence-abstraction-divergent-implementation" target="_blank">define flag</a> 來區隔,例如:以 Player 來説,UWP 使用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.media.playback.mediaplayer" target="_blank">Windows.Media.Playback.MediaPlayer</a>;Android 使用 <a href="https://developer.xamarin.com/api/type/Android.Media.MediaPlayer/" target="_blank">Android.Media.MediaPlayer</a>;iOS 使用 <a href="https://developer.xamarin.com/api/type/AVFoundation.AVPlayer/" target="_blank">AVFoundation.AVPlayer</a>;<br />
我覺得這就是 <a href="https://platform.uno/" target="_blank">Uno Platform</a> 最容易入門的重點,很多處理都依賴熟悉的 .NET 與 UWP 開發技術,除非您需要使用到 Xamarin (或 Xamarin.Forms) 的内容才會需要。 <br />
<br />
[<b>範例程式</b>]<br />
<a href="https://github.com/poumason/DotblogsSampleCode/tree/master/DotblogsSampleCode/28-KKYoutubeUnoApp" target="_blank">28-KKYoutubeUnoApp</a>,使用時如果遇到 Android 因爲路徑過長無法編譯,請移動到 C:\ 下重新建立。<br />
======<br />
<a href="https://platform.uno/" target="_blank">Uno Platform</a> 確實讓習慣開發 UWP 的人更快入門。<br />
它目前是 Preview 很多問題,所以 debug 或錯誤排除相對變困難許多,如果沒有一點 Xamarin (或 Xamarin.Forms) 的概念,要切換不同平臺觀念找到對應的 namespaces 需要花一點時間。 <br />
我期待它的發展,希望有幫助到找跨平臺開發 solution 的人有一些基本的觀念。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://platform.uno/" target="_blank">Uno Platform</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=nventivecorp.uno-platform-addin" target="_blank">Uno Platform Solution Templates</a></li>
<li><a href="https://dotblogs.com.tw/billchung/series/1?qq=Uno%2520Platform%2520%E5%81%B5%E6%9F%A5%E9%9A%8A" target="_blank">Uno Platform 偵查隊</a></li>
<li><a href="https://github.com/nventive/Uno.Windows-universal-samples" target="_blank">nventive/Uno.Windows-universal-samples</a></li>
<li><a href="https://hackernoon.com/cross-platform-mobile-apps-with-net-and-uno-dee2b024281d" target="_blank">Cross Platform Mobile Apps with .NET and Uno</a></li>
<li><a href="https://hackernoon.com/introduction-to-webassembly-for-the-uno-platform-part-1-61c0db29de28" target="_blank">Introduction to WebAssembly for the Uno Platform (Part 1)</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-87807787630822955172018-11-17T12:06:00.000+08:002018-11-17T12:06:22.595+08:00UWP - 抓取 ListView 滾動到哪一個 group 分類與加大虛擬化的項目數量筆記介紹怎麽抓到目前 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">ListView</a> 滾動到哪個 group 分類與加大虛擬化的項目數量。<br />
<a name='more'></a><br />
使用 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">ListView</a> 時,我建議先讀下面幾篇介紹:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">List view and grid view</a></li>
<li>使用 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/item-templates-listview" target="_blank">ItemTemplate 調整項目的顯示方式</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/listview-and-gridview-data-optimization" target="_blank">ListView 和 GridView 資料虛擬化</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/scroll-controls" target="_blank">Scroll viewer controls</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/optimize-gridview-and-listview" target="_blank">ListView 與 GridView UI 最佳化</a></li>
</ul><br />
下面説明本篇的主要内容:<br />
<b>捕捉 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listview" target="_blank">ListView</a> 滾動到哪一個 group 分類</b><br />
什麽例子會需要做這樣的處理,例如:集合中的群組顯示,行事曆日期的顯示...等,用來表示目前瀏覽的項目到哪一個群組了。<br />
要如何做到呢?<u>給與每一個 item 有一個 group name,配合 scroll 時抓到的每一個 item 來判斷到了哪一個 group</u>。<br />
看到這裏是否覺得跟 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listview" target="_blank">ListView</a> 做 group 的機制一樣呢?關於 Group 的做法可以參考 <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Data.CollectionViewSource" target="_blank">CollectionViewSource</a>。<br />
<br />
舉例利用日期與節目名稱當資料來源,用戶滾動 scroll bar 時,通過 ListView 頂端的 item 來影響目前閲讀的日期:<br />
1. 準備 UI XAML 擺放日期與節目清單:<br />
<pre class="code prettyprint"><code class="xml"><Grid>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ListView Grid.Row="0" ItemsSource="{x:Bind ViewModel.GroupDateTime}" ScrollViewer.HorizontalScrollBarVisibility="Visible" ScrollViewer.HorizontalScrollMode="Enabled" Margin="10,0" SelectedIndex="{x:Bind ViewModel.DateSelectedIndex, Mode=TwoWay}">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<ItemsStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="18" Margin="5,10" />
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<ListView Grid.Row="1" x:Name="PrgramsListView" ItemsSource="{x:Bind ViewModel.ProgramCollection}" Loaded="ListView_Loaded">
<ListView.ItemTemplate>
<DataTemplate x:DataType="local:ProgramData">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{x:Bind StartAt}" FontSize="14" Margin="0,0,10,0" /> <TextBlock Text="{x:Bind Title}" FontSize="16" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid></code></pre><br />
2. 從放置節目資料的 ListView 中抓出 ScrollViewer 並注冊 ViewChanged 事件處理 scroll 的距離,並抓取 ListView 中哪一個 Item 已經走到了 ScrollViewer 的 top position(0,0),並對應回去 Calendar 的項目:<br />
<pre class="code perttyprint"><code class="language-csharp">private void ListView_Loaded(object sender, RoutedEventArgs e)
{
// 抓出 ListView 中的 ScrollViewer, 並注冊處理 ViewChanged 事件
scrollViewer = GetScrollViewer(PrgramsListView);
scrollViewer.ViewChanged += ScrollViewer_ViewChanged;
}
private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
// 檢查哪一個 item 已經滾動到 scroll viewer 的 top (positon(0,0)) 的位置
for (int i = 0; i < PrgramsListView.Items.Count; i++)
{
var item = PrgramsListView.ContainerFromIndex(i) as ListViewItem;
if (item == null)
{
continue;
}
// 先用 TransformToVisual 抓出 item 在 ListView 的位移資訊,
// 再用 TransformPoint 根據位移資訊轉成座標
var positionToTop = item.TransformToVisual(PrgramsListView).TransformPoint(new Point(0, 0));
// 如果剛好到了 0 點,通知 ViewModel 執行 SelectedIndex
if (positionToTop.Y >= 0)
{
ViewModel.OnScrollTo(i);
break;
}
}
}
private ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer)
{
return depObj as ScrollViewer;
}
// 利用 VisualTreeHelper 抓出需要的 UIElement
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null)
{
return result;
}
}
return null;
}</code></pre>效果如下圖:<br />
<a href="https://3.bp.blogspot.com/-J2BV3RQIhTA/W-xNnsUn4wI/AAAAAAAAA4A/KMN5bPH9d8QoVYgqmMzTAyJBnJb_hGnxQCLcBGAs/s1600/Untitled.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="688" data-original-width="502" height="400" src="https://3.bp.blogspot.com/-J2BV3RQIhTA/W-xNnsUn4wI/AAAAAAAAA4A/KMN5bPH9d8QoVYgqmMzTAyJBnJb_hGnxQCLcBGAs/s400/Untitled.png" width="291" /></a><br />
範例幫助找到每個分類的頭,最後一個項目可能會找不到,因爲數量過少可能造成 top position 的檢查無法跑到而造成不會顯示到某個分類。<br />
<br />
<b>關鍵技術</b>:<br />
<table border="1" style="width: 100%px;"><tbody>
<tr><td><b>Class</b></td><td><b>Property/Method</b></td><td><b>Description</b></td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.UIElement" target="_blank">UIElement</a></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement.transformtovisual#Windows_UI_Xaml_UIElement_TransformToVisual_Windows_UI_Xaml_UIElement_" target="_blank">TransformToVisual(UIElement visual)</a></td><td>回傳可用於將座標從 UIElement 轉換為指定物件的轉換物件。<br />
例如,抓取 StackPanel 中 TextBlock 的座標轉換物件。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.generaltransform" target="_blank">GeneralTransform</a></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.generaltransform.transformpoint#Windows_UI_Xaml_Media_GeneralTransform_TransformPoint_Windows_Foundation_Point_" target="_blank">TransformPoint</a></td><td>使用此轉換物件的邏輯轉換指定的點, 並返回結果。邏輯説明:<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.media.generaltransform.transformpoint#remarks" target="_blank">Remarks</a></td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.ItemsControl" targert="_blank">ItemsControl</a></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemscontrol.containerfromindex#Windows_UI_Xaml_Controls_ItemsControl_ContainerFromIndex_System_Int32_" target="_blank">ContainerFromIndex</a></td><td>從 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemcollection">ItemCollection</a> 清單中抓出特定 index 的 item (也是容器)。<br />
由於 ListView 繼承了 ItemControl,所以可以從 Container 中找到 Item 來比對,<br />
進一步影響我們要調整的畫面。</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemscontrol.itemspanel#Windows_UI_Xaml_Controls_ItemsControl_ItemsPanel" target="_blank">ItemsPanel</a></td><td>編輯獲取或設置定義控制項佈局的面板的範本。</td></tr>
</tbody></table><br />
<br />
<b>加大虛擬化的項目數量</b><br />
由於 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listview" target="_blank">ListView</a> 或 <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.GridView" target="_blank">GridView</a> 支援虛擬化(virtualization),減少 UI Thread 在遇到大量資料時同時繪製而效能不好,如果使用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listviewbase.scrollintoview" target="_blank">ScrollInToView</a> 超過目前可視與虛擬化範圍則會造成功能失效無法滾動到特定位置。<br />
參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-gridview-and-listview#update-items-incrementally" target="_blank">ListView and GridView UI optimization</a> 虛擬化中兩個重要元素:<span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.ItemsStackPanel" target="_blank">ItemsStackPanel</a></span> 與 <span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.ItemsWrapGrid" target="_blank">ItemsWrapGrid</a></span> 負責虛擬化的處理,如果您使用的不是這兩個就不會有虛擬化的效果,如:<a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/optimize-gridview-and-listview#ui-virtualization" target="_blank">虛擬化</a>的説明。運作方式如下圖:<br />
<a href="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/00/41/23/metablogapi/6507.recycling_2B71634B.png" imageanchor="1" ><img border="0" src="https://msdnshared.blob.core.windows.net/media/MSDNBlogsFS/prod.evol.blogs.msdn.com/CommunityServer.Blogs.Components.WeblogFiles/00/00/00/41/23/metablogapi/6507.recycling_2B71634B.png" width="640" height="367" data-original-width="800" data-original-height="459" /></a><br />
從上圖 Realized Items 代表虛擬化的範圍,Visible Window 代表可視範圍,這樣做可讓 UI thread 不需要大量繪製内容(降低 CPU 與 Memory),隨著用戶滾動範圍 Realized Items 會變成 Unrealized Items 互相交換來顯示内容。更多虛擬化的説明可以參考:<a href="https://blogs.msdn.microsoft.com/alainza/2014/09/03/listview-basics-and-virtualization-concepts/" target="_blank">ListView basics and virtualization concepts</a>。<br />
而 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listviewbase.scrollintoview" target="_blank">ScrollInToView</a> 的對象如果是 Unrealized item 就無法移動了,因爲它更不還沒出現在 UI 裏面。要解決這樣的問題,可以透過 <u>加大虛擬化的數量</u>,在 <span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.ItemsStackPanel" target="_blank">ItemsStackPanel</a></span> 與 <span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.ItemsWrapGrid" target="_blank">ItemsWrapGrid</a></span> 中有一個屬性: <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.cachelength#Windows_UI_Xaml_Controls_ItemsWrapGrid_CacheLength" target="_blank">CacheLength</a>,可以藉由調整它來加到 realized item。<br />
<table border="1" style="width: 100%"><tr><td><b>Name</b></td><td><b>Description</b></td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.cachelength#Windows_UI_Xaml_Controls_ItemsWrapGrid_CacheLength" target="_blank">CacheLength</a></td><td>獲取或設置 viewport 外的緩衝區大小, 以 viewport size 的倍數為值。(預設 4.0)<br />
為改善 scrolling performance,<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid" target="_blank">ItemsWrapGrid</a> 建立並緩存項目來支援屏幕内外的顯示。<br />
<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.cachelength#Windows_UI_Xaml_Controls_ItemsWrapGrid_CacheLength" target="_blank">CacheLength</a> 設定熒幕外的緩存大小,讓可視範圍的内容倍速增加。<br />
<b>注意</b> 設置較小的緩存加快啓動時間,也可以設定較大緩存優化滾動性能,<br />
但加大緩存也代表記憶體用量會增加,建議只用在必要的地方,避免效能不好。<br />
</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.firstcacheindex" target="_blank">FirstCacheIndex</a></td><td>取得目前緩存中的第一個項目在資料集合的 index。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.firstvisibleindex" target="_blank">FirstVisibleIndex</a></td><td>獲取螢幕上第一項的資料集合中的索引。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.lastcacheindex" target="_blank">LastCacheIndex</a></td><td>獲取緩存中最後一項的資料集合中的索引。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.itemswrapgrid.lastvisibleindex" target="_blank">LastVisibleIndex</a></td><td>獲取螢幕上最後一項的資料集合中的索引。</td></tr>
</table>例如:<span class="inline-code">CacheLength = 4 </span>,4 的倍數,資料集合有 30 個項目,可視範圍有 10 個,那就是 30*10*4 = 1200,該 ListView 就需要預留這麽大的範圍。建議可以減少 UI 元素本身的設計或是把資料量減少藉由 Load more 的方式降低載入的時間與滾動的效能。<br />
細節可以參考 <a href="http://www.arashadbm.com/post/uwp-listview-tips/" target="_blank">Uwp ListView Tips And Common Mistakes</a>。<br />
======<br />
開發 UWP 使用 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">ListView</a> 非常頻繁,所以很容易忽略細節,希望這篇有幫助到大家。<br />
操作 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.listview" target="_blank">ListView</a> 與 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.gridview" target="_blank">GridView</a> 時,建議閲讀 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">List view and grid view</a> 與 <a href="https://docs.microsoft.com/en-us/windows/uwp/debug-test-perf/optimize-gridview-and-listview" target="_blank">ListView and GridView UI optimization</a>,有助於如何設計與調整效能。<br />
謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://blogs.msdn.microsoft.com/alainza/2014/09/03/listview-basics-and-virtualization-concepts/" target="_blank">ListView basics and virtualization concepts</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/listview-and-gridview" target="_blank">List view and grid view</a></li>
<li><a href="https://social.msdn.microsoft.com/Forums/sqlserver/en-US/95dc79fe-ceb8-4d07-a28a-c59ca38412e9/uwpchow-to-set-focus-on-first-item-of-a-listview?forum=wpdevelop" target="_blank">[UWP][C#]How to set focus on first item of a ListView?</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/optimize-gridview-and-listview" target="_blank">ListView 與 GridView UI 最佳化</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/listview-and-gridview-data-optimization" target="_blank">ListView 和 GridView 資料虛擬化</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/dotnet/framework/wpf/controls/how-to-use-triggers-to-style-selected-items-in-a-listview" target="_blank">操作說明:使用觸發程序來設定 ListView 中所選項目的樣式</a></li>
<li><a href="https://www.experts-exchange.com/questions/28898763/WPF-getting-the-indices-of-ListView-displayed-items.html" target="_blank">WPF getting the indices of ListView displayed items</a></li>
<li><a href="https://social.msdn.microsoft.com/Forums/en-US/22e96f78-2fb9-464a-af23-57a81a782a7d/how-to-get-listview-visible-items?forum=winappswithnativecode" target="_blank">How to get ListView visible items?</a></li>
<li><a href="https://channel9.msdn.com/Events/Build/2013/3-158" target="_blank">Dramatically Increase Performance when Users Interact with Large Amounts of Data in GridView and ListView</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/optimize-xaml-loading" target="_blank">最佳化您的 XAML 標記</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/debug-test-perf/optimize-your-xaml-layout" target="_blank">最佳化您的 XAML 版面配置</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-81009345122602826922018-10-21T11:17:00.001+08:002018-10-21T11:17:18.368+08:00UWP - 開發 Custom Theme在 WPF 開發 custom theme 可在 XAML 使用 <a href="https://docs.microsoft.com/zh-tw/dotnet/framework/wpf/advanced/dynamicresource-markup-extension" target="_blank">DynamicResource</a> 的機制,動態更換定義好的 Theme <a href="https://docs.microsoft.com/zh-tw/dotnet/api/system.windows.resourcedictionary?view=netframework-4.7.2" target="_blank">ResourceDictionary</a>。<br />
這篇介紹在 UWP 要怎麽做到這樣的效果。<br />
<a name='more'></a><br />
首先介紹基本的定義:<br />
<ul><li>Theme Resources 依賴系統 theme 設定或是從 App 修改來使用,分成:Light, Dark 與 HighContrast。</li>
<li>Theme resources 與 Static resources 的差別:<div><ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/themeresource-markup-extension" target="_blank">{ThemeResource} markup extension</a><br />
<div><ol><li>當 App 啓動,從 App 調整主題或是從設定調整主題時,利用 {ThemeResource} 的對象都會被觸發更新;</li>
<li>UWP SDK 定義了系統與 App 共用的 <span class="inline-code">themeresources.xaml</span>,檔案路徑: <span class="inline-code">\(Program Files)\Windows Kits\10\DesignTime\CommonConfiguration\Neutral\UAP\<sdk version>\Generic</span>,各種 theme resources 都可以在裏面找到;</li>
<li>系統啓動時會載入系統的 ThemeResource 到記憶體(而不是用上述的檔案或是複製一份到 App 裏),因此,App 會自動參考記憶體的内容,不需要額外定義。除非是 App 自定義的則會 merge 進去;</li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/staticresource-markup-extension" target="_blank">{StaticResource} markup extension</a><div>從已定義的 XAML 資源(<a href="https://msdn.microsoft.com/library/windows/apps/br208794" target="_blank">ResourceDictionary</a>)中,利用 key 取得值來給 XAML 屬性使用;<br />
StaticResource 跟 ThemeResource不同,它只有在 App 啓動時被載入 XAML 定義時才會觸發,之後則不會再更新;<br />
</div></li>
</ul></div></li>
<li>自定義 themes resources 需要參考:<a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-theme-resources#guidelines-for-custom-theme-resources" target="_blank">Guidelines for custom theme resources</a> 的説明:<div><ol><li>定義 Light 與 Dark 是基本的,建議額外定義 HighContrast;雖然可定義一個 Default 的 <a href="https://msdn.microsoft.com/library/windows/apps/br208794" target="_blank">ResourceDictionary</a>,但建議使用明確的名字會比較好;</li>
<li>使用 Styles, Setters, Control templates, Property setters 與 Animation 時換成 {ThemeResource} markup extension;</li>
<li>切記在定義的 ThemeDictionaries 中不要使用 {ThemeResource} markup extension,要改用 {StaticResource} markup extension;</li>
<li>如果遇到 theme 的 exception 可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-theme-resources#troubleshooting-theme-resources" target="_blank">Troubleshooting theme resources</a> 排除問題;</li>
</ol></div></li>
<li>Resource 與 ResourceDictionary<div><ul><li>Resources 被定義在 ResourceDictionary 中,通常是在獨立的檔案或是 Page 的最前面,藉由 key 與 StaticResource markup extension 或 ThemeResource markup extension來取得;</li>
<li>Resources 可能是 string 或其他可分享的物件,例如:styles, templates, brushes 與 colors;而 controls, shapes 與其他 <a href="https://msdn.microsoft.com/library/windows/apps/br208706" target="_blank">FrameworkElemets</a> 是不能被分享使用的,詳細的差別可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/resourcedictionary-and-xaml-resource-references#xaml-resources-must-be-shareable" target="_blank">XAML resources must be shareable</a>;範例如下:<div><pre class="code prettyprint"><code class="XML"><Page>
<Page.Resources>
<SolidColorBrush x:Key="myFavoriteColor" Color="green"/>
<x:String x:Key="greeting">Hello world</x:String>
</Page.Resources>
<TextBlock Foreground="{StaticResource myFavoriteColor}" Text="{StaticResource greeting}" VerticalAlignment="Top"/>
<Button Foreground="{StaticResource myFavoriteColor}" Content="{StaticResource greeting}" VerticalAlignment="Center"/>
</Page></code></pre></div></li>
<li>所有的 resources 都要有一個 key,例如:<span class="inline-code">x:Key="mystring"</span>,或是其他方式定義:<div><ul><li><a href="https://msdn.microsoft.com/library/windows/apps/br208849" target="_blank">Style</a> 與 <a href="https://msdn.microsoft.com/library/windows/apps/br209391" target="_blank">ControlTemplate</a> 必須有 <b>TargetType</b>,代表適用的對象,如果沒有給 key,預設會套入所有指定的對象。</li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/br242348" target="_blank">DataTemplate</a> 有 <b>TargetType</b> 可以套入設定的對象。</li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/mt204788" target="_blank">x:Name</a> 常被與 x:Key 比較,差別在 x:Name 會在 code behind 中被建立。因此 x:Name 的效率不容 x:Key 好,因爲每個項目需要在 Page Loaded 時被初始化。</li>
<li>StaticResource 只能使用字串名稱(x:Name 或 x:Key)檢索資源。但當 control 沒有設定 Style/ContentTemplate/ItemTemplate 時 XAML framework 會查找隱式樣式資源(使用 TargetType)</li>
<li>也可以在程式碼中加入應用程式資源,需要注意:<br />
<div><ul><li>定義在 Page 中的只有該 Page 内可以用,如果要共用建議放到 App.xaml 中定義;</li>
<li>要在程式完成執行前就把 resources 加入,避免有頁面要用到;</li>
<li>無法在 App.xaml.cs 的建構子加入 resources;</li>
</ul>如果在 Application.OnLaunched 時加入 resources 就可以避免上面二個問題。</div></li>
</ul></div></li>
<li>由於任何 FrameworkElement 都擁有一個 ResourceDictionary,因此,XAML 在處理 Resources 的定義時,以最靠近 Element 的定義為主,例如:Page 的 ResourceDictionary 定義了某個值,在 Grid 也定義了相同 Key,那再 Grid 中的 TextBox 使用的 Key 則是用 Grid 中定義的爲主。如果您是在程式裏面使用,則需注意來源者是誰,例如:<span class="inline-code">(string)this.Resource["greeting"];</span> 拿到的可能是 control 或是 page 的 resources。</li>
<li>合并 resource dictionaries:<div>利用 <a href="https://msdn.microsoft.com/library/windows/apps/br208801" target="_blank">ResourceDictionary.MergedDictionaries</a> 將多份 resource dictionary xaml 合并起來,如下:<pre class="code pretty"><code class="XML"><Page.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Dictionary1.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Page.Resources></code></pre>同樣地,載入檔案的順序也影響使用 resources 的順序,相同的 key 最後被載入的 resources 檔案優先使用。</div></li>
<li>Theme resources 與 theme dictionaries:<div>ThemeResource 與 StaticResource 相似,差別在 Theme 改變時會重新做資源更新的查詢。<br />
Theme dictionaries 是特殊的合并字典,根據目前裝置上使用的 theme 保留不同的資源。<br />
可利用 <a href="https://msdn.microsoft.com/library/windows/apps/br208807" target="_blank">ResourceDictionary.ThemeDictionaries</a> 將定義好的 xaml resource file 加入,並設定對應的 key,例如:<pre class="code prettyprint"><code class="XML"><Page>
<Page.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary Source="Dictionary1.xaml" x:Key="Light"/>
<ResourceDictionary Source="Dictionary2.xaml" x:Key="Dark"/>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Page.Resources>
<TextBlock Foreground="{StaticResource brush}" Text="hello world" VerticalAlignment="Center"/>
</Page></code></pre></div></li>
</ul></div></li>
<li>如何使用 Theme resources 可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-theme-resources#guidelines-for-using-theme-resources" target="_blank">guidelines for using theme resources</a>。themeresources.xaml 定義多種 resources,例如:Style 可用在文字控制項目或其他。</li>
</ul>更多詳細可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/xaml-overview" target="_blank">XAML overview</a>。<br />
<br />
整理上述的重點:<br />
參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/resourcedictionary-and-xaml-resource-references" target="_blank">ResourceDictionary and XAML resource references</a> 的介紹,自定義的 resources (style, template, container, etc ...) 設定唯一的 key 就可以加入到 <a href="https://www.blogger.com/blogger.g?blogID=2649688415868412622" target="_blank">ResourceDictionary</a>,使用 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/staticresource-markup-extension" target="_blank">{StaticResource} markup extension</a> 或 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/themeresource-markup-extension" target="_blank">ThemeResource</a> 載入設定的内容。<br />
<br />
藉由下面範例,介紹動態切換 themes 的處理:<br />
<b>利用 <span class="inline-code"><a href="https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/ThemeListener" target="_blank">ThemeListener</a></span> 抓取系統 theme 的改變,來調整為自定義的 theme resources</b>:<br />
<pre class="code prettyprint"><code class="csharp">private readonly ThemeListener themeListener;
public MainPage()
{
themeListener = new ThemeListener();
themeListener.ThemeChanged += ThemeListener_ThemeChanged;
}
private void ThemeListener_ThemeChanged(ThemeListener sender)
{
// 取得用戶變換的 theme 類型: Dark 或 Light
Debug.WriteLine(sender.CurrentThemeName);
}
</code></pre>要注意,App 啓動時 <span class="inline-code">FrameworkElement.RequestTheme</span> 預設使用 Default,而不是我們熟悉的 Dark 或 Light。<br />
因此,定義 Resources 要特別注意,分別為兩個定義,如下:<br />
<pre class="code prettyprint"><code class="XML"><Application.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<!-- 為 3 個模式定義 theme resource -->
<ResourceDictionary x:Key="Default">
<Color x:Key="ButtonBaseBorder">#FFFF0000</Color>
<!-- 要記得使用 ThemeResource extension 來指定對象 -->
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Light">
<Color x:Key="ButtonBaseBorder">#FFEFFF00</Color>
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<Color x:Key="ButtonBaseBorder">#FF0091FF</Color>
<SolidColorBrush x:Key="ButtonColor1" Color="{ThemeResource ButtonBaseBorder}" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</Application.Resources></code></pre>利用 <a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/themeresource-markup-extension" target="_blank">{ThemeResource} markup extension</a> 來指定自定義的 key:<span class="inline-code"><Button Content="change theme" Background="{ThemeResource ButtonColor1}" /></span>。這樣一來就可以支援用戶從設定切換 Dark/Light 時跟著變換不同的顔色。<br />
如果要設定 app 變成特定的 theme,可利用 <span class="inline-code">Application.Current.RequestedTheme = ApplicationTheme.Dark;</span>。<br />
<br />
重點元素:<br />
<table border="1"><tr><td>名稱</td><td>説明</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.frameworkelement.requestedtheme#Windows_UI_Xaml_FrameworkElement_RequestedTheme" target="_blank">FrameworkElemnt.RequestedTheme</a></td><td>設定或取得該 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement" target="_blank">UIElement</a> 的 UI theme,設定之後會覆寫 app-level 的 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.application.requestedtheme#Windows_UI_Xaml_Application_RequestedTheme" target="_blank">RequestedTheme</a></td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Application#Windows_UI_Xaml_Application_RequestedTheme" target="_blank">Application.Current.RequestedTheme</a></td><td>由於 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.application" target="_blank">Applicaiton</a> 管理 app-scoped 資源與生命周期。<br />
因此,<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.application.requestedtheme#Windows_UI_Xaml_Application_RequestedTheme" target="_blank">RequestTheme</a> 負責管理 App 的 theme。<br />
它與 <a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/helpers/themelistener" target="_blank">ThemeListener</a> 類似。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/windows/communitytoolkit/helpers/themelistener" target="_blank">ThemeListener</a></td><td>可設定目前 Application theme,並監控 system theme 被改變時會發出事件。<br />
可安裝 <span class="inline-code">Microsoft.Toolkit.Uwp.UI.Helpers</span> 來使用它。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.elementtheme" target="_blank">ElementTheme</a></td><td>代表特定 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement" target="_blank">UIElement</a> 的 theme。與 <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.ApplicationTheme" target="_blank">ApplicationTheme</a> 的層級不一樣,只會影響該 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.uielement" target="_blank">UIElement</a>。</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.ApplicationTheme" target="_blank">ApplicationTheme</a></td><td>代表 app 的 theme。值由:Dark 與 Light。</td></tr>
</table><br />
[<b>補充</b>]<br />
<ul><li>參考 <a href="http://metro.excastle.com/xaml-system-brushes" target="_blank">System brushes</a> 知道那些 resouces 會跟著 theme 改變時調整</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.ResourceDictionary#Windows_UI_Xaml_ResourceDictionary_ThemeDictionaries" target="_blank">ResourceDictionary</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.dependencyobject" target="_blank">DependencyObject</a></li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/br209391" target="_blank">ContentTemplate</a></li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/hh738505" target="_blank">VisualStateManager.VisualStateGroups</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.datatemplate" target="_blank">DataTemplate</a></li>
</ul>======<br />
當然還有別的做法,例如把描述寫成 CSS 再程式裏載入,搭配 <a href="https://dotblogs.com.tw/pou/2015/05/27/151412" target="_blank">Convert</a> 做刷新;或是自定義 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.dependencyobject" target="_blank">DependencyObject</a> 來實作。<br />
以上是介紹如何自訂主題,並提供讓用戶選擇或是跟隨系統的顔色/主題來切換。謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/resourcedictionary-and-xaml-resource-references" target="_blank">ResourceDictionary and XAML resource references</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/xaml-theme-resources" target="_blank">XAML theme resources</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/control-templates" target="_blank">Control templates</a></li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/framework/wpf/app-development/how-to-use-an-application-scope-resource-dictionary" target="_blank">How to: Use an Application-Scope Resource Dictionary</a></li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/mt185595" target="_blank">XAML overview</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/basics/xaml-basics-style" target="_blank">Tutorial: Create custom styles</a></li>
<li><a href="https://www.youtube.com/watch?v=EtXQVZ3iZWk" target="_blank">UWP 029 | XAML Themes</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/templatebinding-markup-extension" target="_blank">{TemplateBinding} markup extension</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/staticresource-markup-extension" target="_blank">{StaticResource} markup extension</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/relativesource-markup-extension" target="_blank">{RelativeSource} markup extension</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/customresource-markup-extension" target="_blank">{CustomResource} markup extension</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xaml-platform/themeresource-markup-extension" target="_blank">{ThemeResource} markup extension</a></li>
<li><a href="https://social.msdn.microsoft.com/Forums/vstudio/en-US/5265b4f6-358a-4bfd-8b43-5304192c917f/uwp-how-to-use-custom-theme?forum=wpdevelop" target="_blank">UWP how to use custom Theme</a></li>
<li><a href="https://xamlbrewer.wordpress.com/2017/02/22/using-a-dynamic-system-accent-color-in-uwp/" target="_blank">Using a Dynamic System Accent Color in UWP</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-53297763897553529702018-10-01T11:52:00.000+08:002018-10-01T11:52:02.992+08:00UWP - 介紹 multi-instance UWP appWindows 10, version 1803 (10.0; Build 17134) 開始,UWP app 支援 multiple instances。<br />
怎麽使用它呢?對於既有的 App 需要做那些調整?藉由這篇來介紹讓大家有些概念。<br />
<a name='more'></a><br />
本篇參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp" target="_blank">Create a multi-instance Universal Windows App</a> 來介紹。<br />
支援 multi-instance 的做法可透過安裝 <a href="https://marketplace.visualstudio.com/items?itemName=AndrewWhitechapelMSFT.MultiInstanceApps" target="_blank">Multi-Instance App Project Templates.Vsix</a> 範本來得到兩種類型:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp#multi-instance-activation-redirection" target="_blank">Multi-instance Redirection</a> UWP App<div>建立 multi-instance app 並帶有額外邏輯控制是否啓動新的 instance 或從已經啓動的 instances 選擇來使用;<br />
例如:希望一次只有一個 instance 可以編輯相同檔案,當開啓檔案時則從已經開啓的 instances 來使用,不再建立 instance。</div></li>
<li>Multi-instance UWP App<div>每次開啓 app 都是建立新 instance。例如:圖片瀏覽器就非常適合。</div></li>
</ul>讓 App 支援 multi-instancing 幾個步驟:<br />
<ol><li><b>Package.appxmanifest 加入宣告</b>:<div>在 namespace prefix 有新的 tag: <span class="inline-code">desktop4</span> 與 <span class="inline-code">iot2</span>,代表只有 Desktop 與 IoT 設備才支援 multi-instancing。<br />
加入 <span class="inline-code">SupportsMultipleInstances</span>,開發過 Background Task 應該知道 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/declare-background-tasks-in-the-application-manifest#declare-where-your-background-task-will-run" target="_blank">SupportsMultipleInstances</a> 宣告,同樣地在 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service#add-an-app-service-extension-to-packageappxmanifest" target="_blank">App Service</a> 的宣告上也支援。參考如下:<br />
<pre class="code prettyprint"><code class="xml"><?xml version="1.0" encoding="utf-8"?>
<Package
xmlns:desktop4="http://schemas.microsoft.com/appx/manifest/desktop/windows10/4"
xmlns:iot2="http://schemas.microsoft.com/appx/manifest/iot/windows10/2"
IgnorableNamespaces="uap mp desktop4 iot2">
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="multi_redirect_instance.App"
desktop4:SupportsMultipleInstances="true"
iot2:SupportsMultipleInstances="true">
</Application>
</Applications>
</Package></code></pre></div></li>
<li><b>建立專用的 Program.cs 取代預設的處理機制</b>:<div>預設 App 被啓動時會自動產生一份 <span class="inline-code">App.g.i.cs</span>,負責啓動前需要處理的事情,如下:<pre class="code prettyprint"><code class="csharp">#if !DISABLE_XAML_GENERATED_MAIN
public static class Program
{
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks","")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
static void Main(string[] args)
{
global::Windows.UI.Xaml.Application.Start((p) => new App());
}
}
#endif</code></pre>參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp#multi-instance-activation-redirection" target="_blank">Multi-instance activation redirection</a> 介紹,改由自定義的 <span class="inline-code">Program.cs</span> 讓 App 啓動前做 multiple-instancing 的邏輯,如下:<br />
1. 在專案檔(*.csproj) 的 <span class="inline-code">DefineConstants</span> 加入宣告:<b>DISABLE_XAML_GENERATED_MAIN</b>,讓啓動時使用自定義 Program.cs;<br />
2. 加入如下的範例程式:<br />
<pre class="code prettyprint"><code class="csharp">public static class Program
{
static void Main(string[] args)
{
// 抓取啓動時給與的參數,例如:從 cmd 來的參數
IActivatedEventArgs activatedArgs = AppInstance.GetActivatedEventArgs();
// 判斷是否有系統推薦的 instance,有的話則直接使用,沒有再走自己的邏輯
if (AppInstance.RecommendedInstance != null)
{
AppInstance.RecommendedInstance.RedirectActivationTo();
}
else
{
// 定義 key 為 instance 注冊,是常見的做法,可按照需求做調整
// key 是唯一值,每次的 instance 都是新的
// key 不是唯一值,instance 就可以重覆使用(redirect)
uint number = CryptographicBuffer.GenerateRandomNumber();
string key = (number % 2 == 0) ? "even" : "odd";
var instance = AppInstance.FindOrRegisterInstanceForKey(key);
if (instance.IsCurrentInstance)
{
// 如果成功注冊,則代表為新的 instance
global::Windows.UI.Xaml.Application.Start((p) => new App());
}
else
{
// 有其他的 instance 注冊相同的 key,則可以 redirect activation 到該 instance
instance.RedirectActivationTo();
}
}
}
}</code></pre>上面的範例是取得亂數指並取 2 的餘數,並給與兩種 key,因此最多只會有 2 個 instance 被建立。您可以調整為其他邏輯,例如:利用檔案完整路徑為 key,讓同一個檔案只有一個 instance 操作;或是利用功能切分等。如果您的目標是每次都開啓新的 instance 就不需要自定義 Program.cs。<br />
</div></li>
</ol>兩個步驟就讓 UWP app 支援 multiple-instancing,下方繼續介紹幾個重要的元素。<br />
<br />
[<b>重要元素</b>]<br />
<b><a href="https://docs.microsoft.com/uwp/api/windows.applicationmodel.appinstance" target="_blank">AppInstance class</a></b><br />
系統會暫存該 app 已經開啓的 instances。<br />
當 app process 在 <span class="inline-code">Main method</span> 被建立時,能利用 AppInstance 選擇是否繼續啓動該 instance 或是導向已經存在的 instance。<br />
另外,shell 能提供推薦的 instance (<span class="inline-code">RecommendedInstance</span>),鼓勵我們選擇是否導向該 instance。<br />
下面兩點要注意:<br />
<ol><li>AppInstance 目的只在 <span class="inline-code">Main method</span> 中使用,如果到其他地方使用有可能拿到的屬性是 null 或是使用方法是 failed;</li>
<li>任何 instances 被回傳之前,必須利用 <span class="inline-code">FindOrRegisterInstanceForKey</span> 來注冊;</li>
<li>AppInstance 只能在有宣告 <span class="inline-code">SupportsMultipleInstances</span> 的專案,詳細説明:<a href="https://docs.microsoft.com/en-us/uwp/schemas/appxpackage/uapmanifestschema/schema-root" target="_blank">Package manifest schema reference for Windows 10</a>;</li>
</ol><table border="1"><tr><td><b>type</b></td><td><b>name</b></td><td><b>description</b></td></tr>
<tr><td>Properties</td><td>IsCurrentInstance</td><td>app 當前的 instance 是否為已注冊特定 key 的 instance。</td></tr>
<tr><td></td><td>Key</td><td>當前 instance 的 key。</td></tr>
<tr><td></td><td>RecommendedInstance</td><td>shell 建議應用程式啟動的應用程式的實例被重定向。</td></tr>
<tr><td>Methods</td><td>FindOrRegisterInstanceForKey(String)</td><td>搭配 key 注冊 instance 到系統或是找到已注冊該 key 的 instance。</td></tr>
<tr><td></td><td>GetActivatedEventArgs()</td><td>取得當前的 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.activation.iactivatedeventargs" target="_blank">IActivatedEventArgs</a>,如同在 <span class="inline-code">OnActivated</span> 事件收到的内容。</td></tr>
<tr><td></td><td>GetInstances()</td><td>取得當前 app 已經注冊的 instances。</td></tr>
<tr><td></td><td>RedirectActivationTo()</td><td>重新導向 activation 到另一個特定的 instance。</td></tr>
<tr><td></td><td>Unregister() </td><td>更新系統的暫存該 instance。</td></tr>
</table><br />
<b><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp#background-tasks-and-multi-instancing" target="_blank">Background tasks 與 multi-instancing</a></b><br />
<ul><li>Out-of-proc background tasks(跨處理序背景工作)支援 multi-instancing。一般而言,每個 new trigger results 都會產生 background task 的新 instance。雖然從技術面來説多個 background tasks 會在相同的 host process 被處理,但是系統還是會建立多個 instances。</li>
<li>In-proc background tasks 不支援 multi-instancing。</li>
<li>Background audio tasks 不支援 multi-instancing。</li>
<li>當 app 注冊 background task 時,通常先檢查該工作是否已經注冊,然後刪除已經存在再重新注冊,或是不執行任何動作。這是一般的 multi-instancing apps 的處理方式。但 multi-instancing app 可能跟著 instance 注冊不同的 background task name,這樣一來,將造成相同的 trigger 有多個注冊,而且 multiple background task instance 將同時被啓動。</li>
<li>App-service 為每個連綫建的 background task 建立不同的 instance。讓 multi-instance apps 可以擁有自己的 app-service background task。更多介紹可以參考 <a href="https://dotblogs.com.tw/pou/2018/07/14/003047" target="_blank">UWP - 介紹 App Service 與新功能</a>。</li>
</ul><br />
<b><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp#additional-considerations" target="_blank">Additional considerations</a></b><br />
<ul><li>Multi-instancing 支援 UWP apps 運行在 desktop 與 Internet of Things(IoT)。</li>
<li>為避免資源互相爭用問題,multi-instancing apps 必須有機制來分割/同步設定,操作 app-local storage 與其他資源。有些標準的做法,例如: mutexes, semaphores, events ... 等。</li>
<li>如果 Package.appxmanifest 有宣告 <span class="inline-code">SupportsMultipleInstances</span>,它的 extensions 不需要再宣告一次。</li>
<li>除了 background task 或 app service 外,在其他 extensions 宣告了 <span class="inline-code">SupportsMultipleInstances</span>,但主專案沒有宣告則會造成結構描述錯誤。</li>
<li>App 可利用 <a href="https://docs.microsoft.com/windows/uwp/launch-resume/declare-background-tasks-in-the-application-manifest" target="_blank">ResourceGroup</a> 在 manifest 宣告把 multiple backgorund tasks 分組到同一個主機中。但是這與 multi-instancing 會有衝突,因爲每一個被啓動應該分開的 host,因此,app 不能在他們的 manifest 宣告 <span class="inline-code">SupportsMultipleInstances</span> 與 <span class="inline-code">ResourceGroup</span>。</li>
</ul><br />
[<b>注意</b>]<br />
<ul><li>Multi-instancing 雖然支援 JavaScript applications,但不支援 multi-instancing redirection。因此,<a href="https://docs.microsoft.com/uwp/api/windows.applicationmodel.appinstance" target="_blank">AppInstance class</a> 無法使用在此類型的 applications。</li>
<li>如果想讓 instances 互相溝通的話,也許可以建立 App Service 讓 main instance 來負責接受與處理,可以參考: <a href="https://github.com/lprichar/UwpMessageRelay" target="_blank">lprichar/UwpMessageRelay</a>。</li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/api/system.threading.mutex?redirectedfrom=MSDN&view=netframework-4.7.2" target="_blank">Mutex Class</a> 專門用來同步存取被保護的資源(例如:檔案),因爲不同 instance 存取時會有互相衝突的問題,藉由呼叫 <span class="inline-code">Mutex.WaitOne()</span> 鎖住 threads 的存取,等到呼叫 <span class="inline-code">Mutex.ReleaseMutex()</span> 再開放給其他 threads 使用。</li>
<li>目前 Store 不支援 IoT 裝置下載的 app,所以宣告 <span class="inline-code">iot2:SupportsMultipleInstances</span> 只適用與 Appx 獨立安裝。</li>
</ul>======<br />
multiple instances 我覺得最困難的問題在於支援可能互相爭奪,例如:SQLite 的使用就需要特別小心,最簡單就是使用 mutexes。<br />
不過如果過去您開發過 Windows Phone 7.1 的音樂播放機制,對於 two process 的處理一定會非常有感覺。<br />
因爲我自己開發的 App 還沒有需要到讓 app 支援 multiple instances,所以這篇寫的有點簡單。希望對大家還是有些幫助。<br />
<br />
<b>References</b><br />
<ul><li><a href="https://msdn.microsoft.com/en-us/magazine/mt846651.aspx" target="_blank">Universal Windows Platform - Closing UWP-Win32 Gaps</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp" target="_blank">Create a multi-instance Universal Windows App</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-lifecycle" target="_blank">Windows 10 universal Windows platform (UWP) app lifecycle</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-services" target="_blank">Use app services and extensions</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/convert-app-service-in-process" target="_blank">Convert an app service to run in the same process as its host app</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-an-extension" target="_blank">Create and host an app extension</a></li>
<li><a href="https://blog.pieeatingninjas.be/2018/02/06/multiple-instances-support-for-uwp-apps/" target="_blank">Multiple instances support for UWP apps</a></li>
<li><a href="https://blog.pieeatingninjas.be/2018/02/25/multiple-instances-support-for-uwp-apps-part-2-redirection/" target="_blank">Multiple instances support for UWP apps (Part 2): Redirection</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/12/04/extend-desktop-application-windows-10-features-using-new-visual-studio-application-packaging-project/" target="_blank">Extend your desktop application with Windows 10 features using the new Visual Studio Application Packaging Project</a></li>
<li><a href="https://dotblogs.com.tw/pou/2018/05/11/105348" target="_blank">UWP - 同一個 App 顯示多個視窗</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-30753616360179712022018-09-09T18:08:00.000+08:002018-09-09T18:11:13.017+08:00PWA 操作 Windows Runtime APIs在 <a href="https://docs.pwabuilder.com/benefits/2018/02/03/benefits-windows-10.html" target="_blank">Benefits of PWA on Windows 10s</a> 介紹了 PWA 在 Windows 10 的好處,讓我更想知道開發 PWA 的細節。這篇介紹使用 <a href="https://docs.microsoft.com/en-us/uwp/api/" target="_blank">Windows Runtime APIs</a> 的範例。<br />
<a name='more'></a><br />
在開發前,有兩個方式可以方便 debug:<br />
<ol><li>建立 <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps/windows-features#set-up-and-run-your-universal-windows-app" target="_blank">Progressive Web App (Universal Windows)</a>,設定 Start Page 與 <span class="inline-code">WindowsRuntimeAccess = true</span> 如下:<div><pre class="code prettyprint"><code class="xml"><Application Id="App" StartPage="http://localhost:65278/">
<uap:ApplicationContentUriRules>
<uap:Rule Match="http://localhost:65278/" Type="include" WindowsRuntimeAccess="all" />
<uap:Rule Match="https://*.*" Type="include" WindowsRuntimeAccess="none" />
<uap:Rule Match="http://*.*" Type="include" WindowsRuntimeAccess="none" />
</uap:ApplicationContentUriRules>
</Application></code></pre>有宣告 <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps/windows-features#set-application-content-uri-rules-acurs" target="_blank">WindowsRuntimeAccess</a> 才能操作 Windows Runtime APIs。我比較建議使用這個方式。</div></li>
<li>利用 <a href="https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide#microsoft-store-app" target="_blank">Microsoft Edge Developer Tools</a> 幫助我們開發 PWA 與確定那些 APIs 是否能使用</li>
</ol><br />
利用 JavaScript 開發與使用 WinRT APIs 與 C# 大部分相同,但需要注意:<br />
<ul><li>WinRT features 在 Javascript 使用時命名大小寫有些不同,可參考 <a href="https://docs.microsoft.com/en-us/microsoft-edge/windows-runtime/using-the-windows-runtime-in-javascript#casing-conventions-with-windows-runtime-features" target="_blank">Casing Conventions with Windows Runtime Features</a>;</li>
<li>事件注冊改用 string 名稱搭配 <span class="inline-code">addEventListener</span> 或 <span class="inline-code">removeEventListener</span> 來處理,或是<span class="inline-code">event = function(ev) {}</span> 的寫法;<pre class="code prettyprint"><code class="javascript">var locator = new Windows.Devices.Geolocation.Geolocator();
locator.addEventListener(
"positionchanged",
function (ev) {
console.log("Got event");
});</code></pre></li>
<li><a href="https://docs.microsoft.com/en-us/microsoft-edge/windows-runtime/using-windows-runtime-asynchronous-methods" target="_blank">非同步執行模式</a>使用 JavaScript <a href="http://go.microsoft.com/fwlink/p/?LinkId=244434" target="_blank">Promise model</a>,寫法如下:<pre class="code prettyprint"><code class="javascript">client.createResourceAsync(uri, description, item)
// Success.
.then(function(newItem) {
console.log("New item is: " + newItem.id);
});</code></pre>更多詳細的寫法可參考:<a href="https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh700330(v=win.10)" target="_blank">Asynchronous programming in JavaScript</a>。</li>
<li><span class="inline-code">Windows.UI.Xaml</span> 不支援 JavaScript apps,改用 <a href="https://docs.microsoft.com/en-us/microsoft-edge/dev-guide/whats-new" target="_blank">EdageHTML</a> engine 來渲染 CSS 與 HTML</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.webui" target="_blank">Windows.UI.WebUI.WebUIApplication</a> 負責 App 生命周期與相關事件,非常重要。<a href="http://go.microsoft.com/fwlink/p/?linkid=241720" target="_blank">App activated, resume, and suspend using the WRL sample</a> 與 <a href="http://go.microsoft.com/fwlink/p/?linkid=231617" target="_blank">App activate and suspend using WinJS sample</a> 介紹怎麽操作 actived, suspended 的事件。<pre class="code prettyprint"><code class="javascript">Windows.UI.WebUI.WebUIApplication.addEventListener("activated", function (activatedEventArgs) {
// Check activatedEventArgs.kind and respond as needed
});</code></pre></li>
<li>利用 <span class="inline-code">if(window.Windows){}</span> 檢查是否支援 Windows Runtime APIs;或者是多檢查特定的 API:<span class="inline-code">if (window.Windows && Windows.UI.Popups)</span>;</li>
</ul>更多細節可以參考 <a href="https://docs.microsoft.com/en-us/microsoft-edge/windows-runtime/using-the-windows-runtime-in-javascript" target="_blank">Using the Windows Runtime in JavaScript</a> 或是下載範例 <a href="https://github.com/Microsoft/Windows-universal-samples" target="_blank">Windows-universal-samples</a> 中的 JS 範例。<br />
另外可參考 <a href="https://docs.microsoft.com/en-us/uwp/api/" target="_blank">Windows UWP Namespaces</a> 與搭配 <a href="https://docs.microsoft.com/en-us/windows/uwp/index" target="_blank">Universal Windows Platform documentation</a> 補充 UWP 開發的基本觀念。<br />
<br />
列出常用到 PWA 的功能:<br />
<h3>1. 如何支援 Push Notification</h3>Windows Push Notification 的機制可以參考之前的文章:<a href="https://dotblogs.com.tw/pou/2015/05/27/151413" target="_blank">Universal App - 整合 Windows Notification Service (WNS) for Server</a> 與 <a href="https://dotblogs.com.tw/pou/2015/05/27/151414" target="_blank">Universal App - 整合 Windows Notification Service (WNS) for Client</a>。<br />
<b>PWA 中注冊取得 WNS channel uri</b><br />
<ol><li>先到 Microsoft Dev Center 的專案啓動 WNS 服務,如下圖:<div><a href="https://1.bp.blogspot.com/-5bNNibi1GSQ/W5Tihl87LpI/AAAAAAAAA3k/h4mhd3P7XtkeZC2d3CiwG_d42fu2_BP1ACLcBGAs/s1600/wns_server_key.png"><img border="0" data-original-height="296" data-original-width="673" height="280" src="https://1.bp.blogspot.com/-5bNNibi1GSQ/W5Tihl87LpI/AAAAAAAAA3k/h4mhd3P7XtkeZC2d3CiwG_d42fu2_BP1ACLcBGAs/s640/wns_server_key.png" width="640" /></a>Server side 建議參考<a href="https://dotblogs.com.tw/pou/2015/05/27/151413" target="_blank">sample code</a>來測試;</div></li>
<li>利用 VS2017 讓 PWA app 關聯到 Microsoft Dev Center 注冊的專案,更新 package.appxmanifest 的參數;<div>重點在:<span class="inline-code"><Identity Name="PouMason.PROJECT1" Publisher="CN=4CABE714-48E4-AC4F-7F16-A5774B167C16F8" Version="1.1.2.0" /></span></div></li>
<li>加入 JavaScript 操作 <a href="https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh868221(v%3dwin.10)" target="_blank">CreatePushNotificationChannelForApplicationAsync</a> 來得到 channel_uri;<div><pre class="code prettyprint"><code class="javascript">var wnsChannel;
// 注冊 push notification
if (typeof Windows !== 'undefined' &&
typeof Windows.UI !== 'undefined' &&
typeof Windows.UI.Notifications !== 'undefined') {
Windows.Networking.PushNotifications.PushNotificationChannelManager.createPushNotificationChannelForApplicationAsync()
.then(function (newChannel) {
console.log(newChannel);
wnsChannel = newChannel;
wnsChannel.addEventListener("pushnotificationreceived", pushNotificationReceivedHandler, false);
}, function (error) {
console.log(error);
});
}</code></pre></div></li>
<li>處理收到的訊息:<pre class="code prettyprint"><code class="javascript">function pushNotificationReceivedHandler(e) {
var notificationTypeName = "";
var notificationPayload;
var pushNotifications = Windows.Networking.PushNotifications;
// 拆解對應的類型
switch (e.notificationType) {
case pushNotifications.PushNotificationType.toast:
notificationTypeName = "Toast";
notificationPayload = e.toastNotification.content.getXml();
break;
case pushNotifications.PushNotificationType.tile:
notificationTypeName = "Tile";
notificationPayload = e.tileNotification.content.getXml();
break;
case pushNotifications.PushNotificationType.badge:
notificationTypeName = "Badge";
notificationPayload = e.badgeNotification.content.getXml();
break;
}
e.cancel = true;
// 取得 payload 中的内容
var xmlDox = new Windows.Data.Xml.Dom.XmlDocument();
xmlDox.loadXml(notificationPayload);
var textElements = xmlDox.getElementsByTagName("text")
var messageDialog = new Windows.UI.Popups.MessageDialog(notificationTypeName, textElements[0].innerText);
messageDialog.showAsync();
}</code></pre></li>
</ol>如果有開發好的 server,可以在拿到 channel_uri 轉送到 server 保存起來,這裏的範例就不特別説明這一段。<br />
關於 JavaScript 注冊與處理 Push Notification 的事件可以參考 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.networking.pushnotifications.pushnotificationreceivedeventargs" target="_blank">PushNotificationReceivedEventArgs Class</a>。<br />
另外,如果想要讓 App 開啓時收到 Push notification 就做處理,建議可以把注冊的邏輯放到 Service Worker 裏面,讓背景來處理。<br />
更多細節可參考 <a href="https://blogs.msdn.microsoft.com/appconsult/2018/06/07/push-notifications-in-a-pwa-running-on-windows-10/" target="_blank">Push notifications in a PWA running on Windows 10</a> 的範例。<br />
<br />
<h3>2. 在 PWA 中發送 Toast</h3><pre class="code prettyprint"><code class="javascript">function (message, iconUrl) {
// 檢查是否支援 Windows Runtime API
if (typeof Windows !== 'undefined' &&
typeof Windows.UI !== 'undefined' &&
typeof Windows.UI.Notifications !== 'undefined') {
var notifications = Windows.UI.Notifications;
// 利用 ToastTemplateType 列舉選擇要用的範本
var template = notifications.ToastTemplateType.toastImageAndText01;
// 轉成 XML
var toastXml = notifications.ToastNotificationManager.getTemplateContent(template);
var textElements = toastXml.getElementsByTagName("text");
textElements[0].appendChild(toastXml.createTextNode(message));
var imageElements = toastXml.getElementsByTagName("image");
// 設定 image 的 src 屬性
var srcAttr = toastXml.createAttribute("src");
srcAttr.value = iconUrl;
var attribs = imageElements[0].attributes;
attribs.setNamedItem(srcAttr);
// 建立 toast 並發送
var toast = new notifications.ToastNotification(toastXml);
var toastNotifier = notifications.ToastNotificationManager.createToastNotifier();
toastNotifier.show(toast);
}
}</code></pre>可以根據需求 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.notifications.toasttemplatetype" target="_blank">ToastTemplateType Enum</a> 調整需要的範本。<br />
<br />
<h3>3. 在 App 的 Local folder 讀寫檔案</h3>參考 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.storage.applicationdata" target="_blank">ApplicationData Class</a> 來操作:<br />
<b>讀取檔案</b><br />
<pre class="code prettyprint"><code class="javascript">function (fileName) {
if (typeof Windows !== 'undefined' &&
typeof Windows.Storage !== 'undefined' &&
typeof Windows.Storage.ApplicationData !== 'undefined') {
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
localFolder.getFileAsync(fileName)
.then(function (file) {
// 抓到檔案並讀取
return Windows.Storage.FileIO.readTextAsync(file);
}, function (ex) {
console.log(ex);
})
.done(function (content) {
console.log(content);
});
}
}</code></pre><b>寫入檔案</b><br />
<pre class="code prettyprint"><code class="javascript">function (fileName, content) {
if (typeof Windows !== 'undefined' &&
typeof Windows.Storage !== 'undefined' &&
typeof Windows.Storage.ApplicationData !== 'undefined') {
var localFolder = Windows.Storage.ApplicationData.current.localFolder;
localFolder.createFileAsync(fileName, Windows.Storage.CreationCollisionOption.replaceExisting)
.then(function (file) {
// 抓到檔案並寫入
return Windows.Storage.FileIO.writeTextAsync(file, content);
})
.done(function () {
console.log("saved.");
});
}
}</code></pre><br />
<h3>4. 操作 User Activity</h3><b>在 time line 加入 activity</b><br />
<pre class="code prettyprint"><code class="javascript">function (activityId) {
if (typeof Windows !== 'undefined' &&
typeof Windows.ApplicationModel !== 'undefined' &&
typeof Windows.ApplicationModel.UserActivities !== 'undefined') {
createActivity(activityId).the(function () {
console.log("done");
});
}
};
async function createActivity(activityId) {
var channel = Windows.ApplicationModel.UserActivities.UserActivityChannel.getDefault();
var activity = await channel.getOrCreateUserActivityAsync(activityId);
if (activity.state == Windows.ApplicationModel.UserActivities.UserActivityState.new) {
activity.visualElements.displayText = "new activity";
activity.activationUri = new Windows.Foundation.Uri('testapp://mainPage?state=new&id=' + activityId);
} else {
activity.visualElements.displayText = "published activity";
activity.activationUri = new Windows.Foundation.Uri('testapp://mainPage?state=published&id=' + activityId);
}
activity.contentInfo = Windows.ApplicationModel.UserActivities.UserActivityContentInfo.fromJson('{ "user_id": "pou", "email": "poumason@live.com"}');
await activity.saveAsync();
var activitySesion = activity.createSession();
}</code></pre>利用 <a href="https://developer.mozilla.org/zh-TW/docs/Web/JavaScript/Reference/Statements/async_function" target="_blank">async function</a> 處理 then 裏面還有非同步方法的複雜寫法。<br />
<br />
<b>處理點擊 User Activity 帶入的 protocol</b><br />
<pre class="code prettyprint"><code class="javascript">if (typeof Windows !== 'undefined' &&
typeof Windows.UI !== 'undefined' &&
typeof Windows.UI.WebUI !== 'undefined') {
Windows.UI.WebUI.WebUIApplication.addEventListener("activated", function (activatedEventArgs) {
console.log(activatedEventArgs);
if (activatedEventArgs.kind == Windows.ApplicationModel.Activation.ActivationKind.protocol ) {
var query = activatedEventArgs.uri.queryParsed;
console.log(query);
for (var i = 0; i < query.length; i++) {
console.log(query[i].name + '=' + query[i].value);
}
}
});
}</code></pre><br />
[<b>範例</b>]<br />
<a href="https://github.com/poumason/DotblogsSampleCode/tree/master/DotblogsSampleCode/27-PWASample" target="_blank">DotblogsSampleCode/27-PWASample/</a><br />
======<br />
以上是我研究 PWA 使用 <a href="https://docs.microsoft.com/en-us/uwp/api/" target="_blank">Windows Runtime APIs</a> 的心得,希望有助於大家快速理解。<br />
如果有寫錯的地方,也歡迎大家留言讓我知道,謝謝大家。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/microsoft-edge/windows-runtime/using-the-windows-runtime-in-javascript" target="_blank">Using the Windows Runtime in JavaScript</a></li>
<li><a href="https://developer.microsoft.com/en-us/windows/pwa" target="_blank">Progressive Web Apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide/debugger/progressive-web-apps" target="_blank">Progressive Web App debugging</a> & <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps/windows-features#set-up-and-run-your-universal-windows-app" target="_blank">Tailor your PWA for Windows</a></li>
<li><a href="https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh700330(v=win.10)" target="_blank">Asynchronous programming in JavaScript</a></li>
<li><a href="https://marketplace.visualstudio.com/items?itemName=johnpapa.pwa-tools" target="_blank">PWA Tools</a> & <a href="https://docs.microsoft.com/en-us/microsoft-edge/devtools-guide#microsoft-store-app" target="_blank">Microsoft Edge Developer Tools</a></li>
<li><a href="https://docs.pwabuilder.com/benefits/2018/02/03/benefits-windows-10.html" target="_blank">Benefits of PWA on Windows 10s</a> & <a href="https://docs.microsoft.com/en-us/uwp/api/" target="_blank">Windows Runtime APIs</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API" target="_blank">Service Worker API</a> & <a href="https://cythilya.github.io/2017/07/16/service-worker/" target="_blank">Service Worker</a></li>
<li><a href="https://www.cronj.com/blog/browser-push-notifications-using-javascript/" target="_blank">Browser push notifications using JavaScript</a> & <a href="https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications" target="_blank">Introduction to Push Notifications</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/dotnet/standard/modern-web-apps-azure-architecture/choose-between-traditional-web-and-single-page-apps" target="_blank">在傳統 Web 應用程式和單頁應用程式 (SPA) 之間作選擇</a></li>
<li><a href="https://developer.microsoft.com/en-us/events/build/content/building-progressive-web-apps" target="_blank">PWA talk at Build 2018 (1:23:05)</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2018/06/07/push-notifications-in-a-pwa-running-on-windows-10/" target="_blank">Push notifications in a PWA running on Windows 10</a></li>
<li><a href="https://social.msdn.microsoft.com/Forums/en-US/48cfb7f3-6d6a-42d8-8629-bd3fffe7bbf1/uwpwhats-is-difference-between-pwa-and-uwp?forum=wpdevelop" target="_blank">[UWP]Whats is difference between PWA and UWP?</a></li>
<li><a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps/microsoft-store" target="_blank">Submit your PWA to the Microsoft Store</a></li>
<li><a href="https://github.com/pwa-builder/manifoldJS/wiki/Getting-Started" target="_blank">ManifoldJS</a> & <a href="https://www.w3.org/TR/appmanifest/" target="_blank">Web App Manifest</a></li>
<li><a href="https://developers.google.com/web/fundamentals/primers/promises" target="_blank">JavaScript Promises: an Introduction</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.foundation.uri" target="_blank">Uri Class</a> & <a href="https://developers.google.com/web/fundamentals/primers/async-functions?hl=zh-tw" target="_blank">異步函數 - 提高 Promise 的易用性</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.data.xml.dom.xmldocument" target="_blank">XmlDocument Class</a></li>
<li><a href="https://blogs.msdn.microsoft.com/appconsult/2018/04/07/progressive-web-apps-on-windows-10-live-tiles-toast-notifications-and-action-center/" target="_blank">Progressive Web Apps on Windows 10: Live Tiles, Toast Notifications and Action Center</a></li>
<li><a href="https://docs.microsoft.com/en-us/previous-versions/windows/apps/hh868244(v%3dwin.10)" target="_blank">Sending push notifications with WNS (XAML)</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.networking.pushnotifications.pushnotificationchannel.pushnotificationreceived" target="_blank">PushNotificationChannel.PushNotificationReceived Event</a></li>
<li><a href="https://developer.mozilla.org/zh-TW/docs/Web/API/EventTarget/addEventListener" target="_blank">EventTarget.addEventListener()</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/winrt-components/brokered-windows-runtime-components-for-side-loaded-windows-store-apps" target="_blank">側載 UWP app 的代理 Windows 執行階段元件</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-31637984489892111532018-08-19T15:03:00.000+08:002018-11-12T14:52:54.482+08:00學習使用 PWABuilder 建立 PWA App 心得<a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">Progress Web Apps on Windows</a> 在 Microsoft Build 2018 被提及,隨著更多公司(ex: Twitter, Uber 等)把服務轉成 PWA 上架到 Store,讓 PWA 更加熱門。本篇介紹 <a href="https://www.pwabuilder.com/" target="_blank">PWABuilder</a> 使用過程遇到的問題與心得。<br />
<a name='more'></a><br />
<a href="https://developer.mozilla.org/en-US/Apps/Progressive" target="_blank">Progress Web Apps(PWAs)</a> 讓開發人員沿用 Web 技術做到接近 native app-like experience,省去需要開發不同平臺的成本。但 PWA 最大難度就是漸進式把 Web site 功能效果做到接近 native apps 的品質。<br />
根據 <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">Progress Web Apps on Windows</a> 的介紹,要做到好的 PWA apps,需要滿足下圖特點:<br />
<div class="separator" style="clear: both; text-align: center;"><a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank"><img border="0" data-original-height="565" data-original-width="964" height="375" src="https://1.bp.blogspot.com/-tMe5v8ANOuQ/W2spw2BjEHI/AAAAAAAAA2c/uS8zj4-r5skmrb1ugcayc6fL9sn0MnEQACLcBGAs/s640/Untitled.png" width="640" /></a></div>支援 offline 跟 push notification 是 PWAs 最基本的需求,進一步則是可從 web search 找到内容並開啓 App 或是分享連結來操作。<br />
<br />
有 <a href="https://www.blogger.com/blogger.g?tab=mj&blogID=2649688415868412622" target="_blank">PWA Builder</a> 幫忙,讓現有的網站容易變成 App 上架到 Store。<br />
往下介紹參考 <a href="https://www.pwabuilder.com/" target="_blank">Generate your Progressive Web App </a> 如何建立 PWA App,並補充需注意的地方:<br />
<ul><li>輸入特定 URL 之後,PWABuilder 會解譯出網站的説明,並顯示需要調整的地方,最常見的問題就是:missing image:<div><img border="0" data-original-height="169" data-original-width="832" height="130" src="https://1.bp.blogspot.com/-7EInaqeNYHI/W3L9oK61MmI/AAAAAAAAA20/VUXhcZ5xP1kZ6T5zvgXUSMh_xE-eckahQCLcBGAs/s640/Untitled.png" width="640" /><br />
雖然不影響封裝,但建議所有圖片都提供省去後面封裝檔案缺圖的問題;</div></li>
<li>在建立 Manifest 時可以搭配 <a href="https://www.w3.org/TR/appmanifest" target="_blank">Web App Manifest</a> 來寫入必要的資訊,其中比較重要的:<div><ul style="list-style-type: circle;"><li><span class="inline-code">name</span> 或 <span class="inline-code">short_name</span>:代表 Web Application 的名稱。</li>
<li><span class="inline-code"><a href="https://www.w3.org/TR/appmanifest/#navigation-scope" target="_blank">scope</a></span>:代表可以瀏覽的範圍,如果沒有填寫會以 <span class="inline-code">start_url</span> 的目錄爲主。例如: start_url 是 /page/welcome.html 那 scope 可以存取 /page/ 以下的内容;詳細可參考 <a href="https://www.blogger.com/blogger.g?tab=mj&blogID=2649688415868412622">4. Navigation scope </a>。</li>
<li><span class="inline-code">display</span>:代表如何呈現在系統上,具有:fullsreen, standalone, minimal-ui, browser。</li>
<li><span class="inline-code">start_url</span>:預設為用戶啓動 web application 要顯示的 URL。</li>
<li><span class="inline-code">icons</span>:圖片的集合,與 PWA 要被安裝在那個平臺有關係,可以利用 <a href="https://www.pwabuilder.com/imageGenerator" target="_blank">App Image Generator</a> 建立各平臺的圖示。</li>
<li><span class="inline-code"><a href="https://www.w3.org/TR/appmanifest/#serviceworker-member" target="_blank">serviceworker</a></span>:定義 service worker 的詳細資訊,例如: <pre class="code prettyprint"><code class="json">"serviceworker": {
"src": "sw.js",
"scope": "/foo",
"update_via_cache": "none"
}</code></pre></li>
</ul></div>定義好的 manifest.json 可以加入到網站的宣告: <span class="inline-code"><meta src="manifest.json" /></span>,方便 PWABuilder 下載,而 Browser 也會參考 manifest.json 的定義安裝 web application。如果安裝成功,在設備的 home screen 就會看到該 web application。<br />
</li>
<li><b><a href="https://docs.pwabuilder.com/what/is/a/pwa/2018/02/03/what-is-a-service-worker.html" target="_blank">Service Workers</a></b><br />
<div>它是讓 browser 可在背景運作的 scripts,在 browser 中利用程式管理 web/HTTP request 作爲 network proxy。<br />
利用下面的範例程式把 service workers 注冊到您的網站:<br />
<pre class="code prettyprint"><code class="javascript">if (navigator.serviceWorker.controller) {
console.log('[PWA Builder] active service worker found, no need to register')
} else {
//Register the ServiceWorker
navigator.serviceWorker.register('pwabuider-sw.js', {
scope: './'
}).then(function(reg) {
console.log('Service worker has been registered for scope:'+ reg.scope);
});
}</code></pre>[<b>注意</b>]<br />
<ul><li>可利用 <a href="https://jakearchibald.github.io/isserviceworkerready/" target="_blank">is ServiceWorker ready</a> 或是 <span class="inline-code">if('serviceWorker' in navigator)</span> 來檢查瀏覽器是否支援</li>
<li>只支援 HTTPS 或 localhost</li>
</ul>Service Worker 的生命周期:<br />
<img alt="service worker lifecycle" height="624" src="https://developers.google.com/web/fundamentals/primers/service-workers/images/sw-lifecycle.png" width="640" /><br />
從上圖可理解注冊 Service Worker 之後,完成 install 後會進入 active 並在背景運作,如果過程失敗它將不會被啓動,但再下一次進去這個網站時會執行一次。<br />
<pre class="code prettyprint"><code class="javascript">//This is the "Offline page" service worker
//Install stage sets up the offline page in the cache and opens a new cache
self.addEventListener('install', function(event) {
var offlinePage = new Request('offline.html');
event.waitUntil(
fetch(offlinePage).then(function(response) {
return caches.open('pwabuilder-offline').then(function(cache) {
console.log('[PWA Builder] Cached offline page during Install'+ response.url);
return cache.put(offlinePage, response);
});
}));
});
//If any fetch fails, it will show the offline page.
//Maybe this should be limited to HTML documents?
self.addEventListener('fetch', function(event) {
event.respondWith(
fetch(event.request).catch(function(error) {
console.error( '[PWA Builder] Network request Failed. Serving offline page ' + error );
return caches.open('pwabuilder-offline').then(function(cache) {
return cache.match('offline.html');
});
}
));
});
//This is a event that can be fired from your page to tell the SW to update the offline page
self.addEventListener('refreshOffline', function(response) {
return caches.open('pwabuilder-offline').then(function(cache) {
console.log('[PWA Builder] Offline page updated from refreshOffline event: '+ response.url);
return cache.put(offlinePage, response);
});
});</code></pre>如上面範例,在 install 完畢之後則加入一個 offline.html 到 cache 裏面。<br />
在 active 狀態時,Service Worker 將控制 scope 下 pages 的交易,因此,當有 http request 出現時會觸發 fetch 事件,可以搭配 <span class="inline-code">event.responseWith()</span> 先檢查是否有 caches ,如果有就可以直接回復,如果沒有才真的轉給 server。如下範例:<br />
<pre class="code prettyprint"><code class="javascript">self.addEventListener('fetch', function(event) {
event.respondWith(
caches.match(event.request)
.then(function(response) {
// Cache hit - return response
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});</code></pre>如果使用 PWABuilder 它會根據您選擇的 Service Worker 需要的功能 (例如: offline page, offline copy of pages, offline copy with backup offline pages, cache-first network 或 advanced pre-cache)產生必要的 js 檔案,記得把它放到您的網站。<br />
更多 Service Worker 的介紹可以參考 <a href="https://cythilya.github.io/2017/07/16/service-worker/" target="_blank">Service Worker</a>。</div></li>
</ul><br />
利用 PWABuilder Web tool 產生 manifest.xml 與選擇 Service Worker 支援的功能後,選擇建立 packages 或是把專案下載下來,如下圖:<br />
<a href="https://2.bp.blogspot.com/-YizAZSTmZhc/W3jnu0y42BI/AAAAAAAAA3M/H4bQchSZpBQVHLC27hhigHD3SE063HqygCLcBGAs/s1600/Untitled.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="377" data-original-width="489" height="307" src="https://2.bp.blogspot.com/-YizAZSTmZhc/W3jnu0y42BI/AAAAAAAAA3M/H4bQchSZpBQVHLC27hhigHD3SE063HqygCLcBGAs/s400/Untitled.png" width="400" /></a><br />
如果您選擇 appx 的下載可以從 zip 中找到 windows.appx,如果不是則需要自己編譯,下面來看有那些步驟。<br />
zip 解開的目錄為:<br />
<pre class="code prettyprint"><code>{app name}-windows10
->\projects
->\PWA
->\Store packages
->\windows10
->\manifest (封裝成 appx 必要的 appxmanifest.xml 與相關圖片)
->\package (封裝成品的目錄)
->\source (專案的 source code, 裏面有 package.appxmanifest 與相關圖片)
->\generationInfo.json
->\manifest.json (這個可以放在網站的宣告)
->\test-install.ps1 (檢查執行安裝前的邏輯)
->\Windows10-next-steps.md
->\serviceWorker1</code></pre><ol><li>利用 Visual Studio 2017 開啓 source 目錄下的 App.jsproj,並綁定 Dev Center 中的 Project 來拿到 <b>StoreKey.pfx</b> 與相關的參數</li>
<li>把 manifest 目錄下的 <span class="inline-code">appxmanifest.xml</span> 中被標記為 INSERT-YOUR-PACKAGE-IDENTITY-NAME-HERE 的值改爲正確的值,包括:PublisherDisplayName,Identity 中的 Name, Publisher, Version</li>
<li>如果您想要更新 App 的 icons,可以利用 <a href="https://www.pwabuilder.com/imageGenerator" target="_blank">App Image Generator</a> 選建立 Windows 10 的圖示,並把它們放到 manifest 與 source 目錄中的 images 目錄裏,並把 <span class="inline-code">resources.pri</span> 讓封裝的時候可以重新建立資源檔</li>
<li>安裝 <a href="https://nodejs.org/" target="_blank">node.js runtime</a> 與 <span class="inline-code">npm install pwabuilder -g</span>,可以參考 <a href="https://docs.pwabuilder.com/quickstart/2018/02/03/quick-start-pwa-using-cli-tools.html" target="_blank">Quick Start PWA using CLI tools</a></li>
<li>利用 PWA CLI tools 下指令來封裝 app:<pre class="code prettyprint"><code>pwabuilder package {app name}-windows10\projects -p windows10 -l debug</code></pre><div>產生的 windows.appx 會放在 package 目錄,這裏不加 <b>-a</b> 的參數到指令裏面,因爲還不需要自動上傳到 Store</div></li>
<li>把 <b>StoreKey.pfx</b> 加入 windows.appx 的憑證,加入憑證之後就可以安裝到 Windows 10 設備:<br />
<pre class="code prettyprint"><code>cd "C:\Program Files (x86)\Windows Kits\10\bin\x86\"
SignTool sign /fd SHA256 /a /f "C:\{app name}-windows10\projects\PWA\Store packages\windows10\package\StoreKey.pfx" "C:\{app name}-windows10\projects\PWA\Store packages\windows10\package\windows.appx"</code></pre></li>
</ol>以上就是利用 PWABuilder 建立可以安裝在 Windows 10 設備的介紹。<br />
下面多補充説明如果把 PWABuilder 建立好的 appx 安裝在 XBOX 上面,網站需要加入那些調整。<br />
<br />
<h3>把 PWA App 放在 Xbox 上執行</h3><a href="https://poumason.blogspot.com/2018/01/uwp-xbox-app-tv-safe.html" target="_blank">UWP - 開發 Xbox App 處理 TV-safe</a> 與 <a href="https://www.blogger.com/UWP%20-%20%E9%96%8B%E7%99%BC%20Xbox%20App%20%E8%99%95%E7%90%86%20TV-safe" target="_blank">UWP - 開發 Xbox App 處理 XY navigation</a> 介紹過在 Xbox 上開發 App 要處理:<br />
<ul><li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/how-to-disable-mouse-mode" target="_blank">如何停用滑鼠模式</a>:<div><ol><li>下載 <a href="https://github.com/Microsoft/TVHelpers/tree/master/tvjs/src/DirectionalNavigation" target="_blank">directionalnavigation-1.0.0.0.js</a> 加到網頁裏面: <span class="inline-code"><script src="directionalnavigation-1.0.0.0.js"></script></span></li>
<li>在 javascript 中加入 <span class="inline-code">navigator.gamepadInputEmulation = "gamepad";</span>。<br />
<b>gamepadInputEmulation</b> 有 3 種模式:預設是 mouse (啓用滑鼠模式);keyboard (停用滑鼠模式,切換用 keyboard 輸入的 DOM 鍵盤事件);gamepad (關閉滑鼠模式,不會產生 DOM 鍵盤事件,改使用 DOM 或 WinRT gamepad APIs)</li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/disable-scaling" target="_blank">如何關閉縮放比例</a>:<div>XAML App 預設調整為 200%, HTML App 則爲 150%。可以關閉並改用裝置的實際像素尺寸 (1910 x 1080 像素)。<br />
JavaScript 的指令:<br />
<pre class="code prettyprint"><code class="javascript">var result = Windows.UI.ViewManagement.ApplicationViewScaling.trySetDisableLayoutScaling(true);</code></pre>或是 CSS:<br />
<pre class="code prettyprint"><code class="css">@media (max-height: 1080px) {
@-ms-viewport {
height: 1080px;
}
} </code></pre></div></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/turn-off-overscan" target="_blank">如何在螢幕邊緣繪製 UI</a>:<div>預設 App 會針對電視保留安全區域(<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#tv-safe-area" target="_blank">TV-safe area</a>),確保内容能夠正常顯示。建議可以關閉,JavaScript 的指令:<pre class="code prettyprint"><code class="javascript">Windows.UI.ViewManagement.ApplicationView.getForCurrentView().setDesiredBoundsMode(Windows.UI.ViewManagement.ApplicationViewBoundsMode.useCoreWindow);</code></pre></div></li>
<li>Javascript 處理 XY navigation 可以參考 <a href="https://github.com/Microsoft/TVHelpers/wiki/Using-DirectionalNavigation" target="_blank">Using DirectionalNavigation</a></li>
</ul>======<br />
<a href="https://developer.mozilla.org/en-US/Apps/Progressive" target="_blank">Progress Web Apps(PWAs)</a> 確實方便,但並不是全部 App 都能這樣取代,需要看 App 本身需求與硬體相依程度,不過大部分 80% 都能使用。尤其是 <a href="https://docs.pwabuilder.com/what/is/a/pwa/2018/02/03/what-is-a-service-worker.html" target="_blank">Service Worker</a> 的邏輯需要特別注意。<br />
另外需要注意開發時,如果需要操作各個平臺的 APIs 只能經由 JavaScript 來互動,我建議可以多補充這方便的内容,有助於判斷那些功能可否實現。希望對大家有所幫助,謝謝。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/microsoft-edge/" target="_blank">Microsoft Edge developer documentation</a></li>
<li><a href="https://developers.google.com/web/fundamentals/primers/service-workers/?hl=zh-tw" target="_blank">服務工作線程:簡介</a></li>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API" target="_blank">Service Worker API</a></li>
<li><a href="https://www.iqvis.com/blog/progressive-web-app-development/" target="_blank">7 REASONS WHY PROGRESSIVE WEB APPS ARE THE FUTURE OF WEB DEVELOPMENT</a></li>
<li><a href="https://github.com/Microsoft/TVHelpers/wiki/Using-DirectionalNavigation" target="_blank">Using DirectionalNavigation</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#tv-safe-area" target="_blank">TV-safe area</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/how-to-disable-mouse-mode" target="_blank">如何停用滑鼠模式</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/disable-scaling" target="_blank">如何關閉縮放比例</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/xbox-apps/turn-off-overscan" target="_blank">如何在螢幕邊緣繪製 UI</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/packaging/packaging-uwp-apps" target="_blank">使用 Visual studio 封裝 UWP app</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/desktop/appxpkg/make-appx-package--makeappx-exe-" target="_blank">App packager (MakeAppx.exe)</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/packaging/sign-app-package-using-signtool" target="_blank">使用 SignTool 簽署應用程式套件</a></li>
<li><a href="https://developer.mozilla.org/en-US/Apps/Progressive" target="_blank">Progress Web Apps(PWAs)</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/get-started/get-started-tutorial-game-js2d" target="_blank">使用 JavaScript 建立 UWP 遊戲</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/get-started/get-started-tutorial-game-js3d" target="_blank">使用 three.js 建立 3D JavaScript 遊戲</a></li>
<li><a href="https://cythilya.github.io/2017/07/16/service-worker/" target="_blank">Service Worker</a></li>
<li><a href="https://www.cronj.com/blog/browser-push-notifications-using-javascript/" target="_blank">Browser push notifications using JavaScript</a></li>
<li><a href="https://developers.google.com/web/ilt/pwa/introduction-to-push-notifications" target="_blank">Introduction to Push Notifications</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-42388779296780328482018-07-22T10:59:00.000+08:002018-07-22T23:01:21.356+08:00UWP - 介紹 Project Rome - 2承接 <a href="https://poumason.blogspot.com/2018/07/uwp-project-rome-1.html" target="_blank">UWP - 介紹 Project Rome - 1</a> 介紹 RemoteSystem 的應用,這篇介紹利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninfo" target="_blank">RemoteSystemSessionInfo</a> 做到多個設備互相交換訊息。<br />
<a name='more'></a><br />
利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninfo" target="_blank">RemoteSystemSessionInfo</a> 可以讓多個設備進入同一個 Session (如同進到包廂),並在裏面方便的交換訊息。<br />
因此,下面範例分別用建立 Session 與加入 Session,最後再説明怎麽交換訊息。<br />
<br />
<b>在使用這些功能前,要記得呼叫 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystem.requestaccessasync#Windows_System_RemoteSystems_RemoteSystem_RequestAccessAsync" target="_blank">RemoteSystem.RequestAccessAsync</a>,讓用戶允許 App 有權限。</b><br />
<br />
<h2>* 利用 RemoteSystemSessionController 建立 Session,並處理相關事件</h2><pre class="code prettyprint"><code class="csharp-language">public async void OnCreateRemoteSystemSessionClick(object sender, RoutedEventArgs e)
{
// 加入 option 限制只有被邀請的人才可以加入
//RemoteSystemSessionOptions options = new RemoteSystemSessionOptions()
//{
// IsInviteOnly = true
//};
sessionController = new RemoteSystemSessionController("today is happy day");
sessionController.JoinRequested += SessionController_JoinRequested;
// 建立一個 Remote Session
RemoteSystemSessionCreationResult result = await sessionController.CreateSessionAsync();
if (result.Status == RemoteSystemSessionCreationStatus.Success)
{
currentSession = result.Session;
currentSession.Disconnected += (obj, args) =>
{
// 代表從該 Session 離線了
Debug.WriteLine($"session_disconnected: {args.Reason.ToString()}");
};
// 注冊訊息通道做資料傳遞
RegistMessageChannel(currentSession, currentSession.DisplayName);
// 註冊有哪些參與者加入或離開
SubscribeParticipantWatcher(currentSession);
// 假設有選到特定的設備,也可以直接發邀請給對放
if (currentRemoteSystem != null)
{
var inviationResult = await currentSession.SendInvitationAsync(currentRemoteSystem);
}
}
}
private async void SessionController_JoinRequested(RemoteSystemSessionController sender, RemoteSystemSessionJoinRequestedEventArgs args)
{
var deferral = args.GetDeferral();
var remoteSystem = args.JoinRequest.Participant.RemoteSystem;
await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
{
var dialog = new MessageDialog($"do you access {remoteSystem.DisplayName} to join the session?");
dialog.Commands.Add(new UICommand("Accept", (cmd) =>
{
args.JoinRequest.Accept();
}));
dialog.Commands.Add(new UICommand("Abort"));
dialog.DefaultCommandIndex = 0;
await dialog.ShowAsync();
});
deferral.Complete();
}
</code></pre><br />
如果是被邀請的參與者,需要註冊 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninvitationlistener" target="_blank">RemoteSystemSessionInvitationListener</a> 來處理被邀請的事件通知,如下範例:<br />
<pre class="code prettyprint"><code class="csharp">private RemoteSystemSessionInvitationListener invitationListener;
public async void OnSubscribeAndHandleInvoke()
{
invitationListener = new RemoteSystemSessionInvitationListener();
// 註冊處理來自其他 RemoteSystem 發出的 RemoteSession 邀請
invitationListener.InvitationReceived += async (sender, args) =>
{
// 未加入前,是利用 RemoteSystemInfo 做 JoinAsync()
RemoteSystemSessionJoinResult joinResult = await args.Invitation.SessionInfo.JoinAsync();
if (joinResult.Status == RemoteSystemSessionJoinStatus.Success)
{
// 注冊訊息通道做資料傳遞
RegistMessageChannel(currentSession, currentSession.DisplayName);
// 註冊有哪些參與者加入或離開
SubscribeParticipantWatcher(currentSession);
}
};
}</code></pre><br />
<h2>* 利用 RemoteSystemSessionWatcher 找到可以加入的 Sessions,並請求加入</h2><pre class="code prettyprint"><code class="csharp-language">public void OnDescoverSessionAsync(object sender, RoutedEventArgs e)
{
// 建立 Watcher 來查看有哪些 Sessions 被建立或是刪除
sessionWatcher = RemoteSystemSession.CreateWatcher();
sessionWatcher.Added += (s, a) => {
// 將找到的 RemoteSystemInfo 加入 UI 顯示
RemoteSystemSessionInfo sessionInfo = a.SessionInfo;
var addedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
sessionList.Add(sessionInfo);
});
};
sessionWatcher.Removed += (s, a) => {
// 將已經結束的 session 從 UI 移除
var removedSession = a.SessionInfo;
var exist = sessionList.Where(x => x.ControllerDisplayName == removedSession.ControllerDisplayName && x.DisplayName == removedSession.DisplayName).FirstOrDefault();
if (exist!= null)
{
var removedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
sessionList.Remove(exist);
});
}
};
sessionWatcher.Updated += (s, a) => {
// 講更新的 RemoteSystemInfo 加入到 UI
var updatedSession = a.SessionInfo;
var exist = sessionList.Where(x => x.ControllerDisplayName == updatedSession.ControllerDisplayName && x.DisplayName == updatedSession.DisplayName).FirstOrDefault();
if (exist != null)
{
var updatedTask = dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
sessionList.Remove(exist);
sessionList.Add(updatedSession);
});
}
};
sessionWatcher.Start();
}</code></pre><br />
選擇找到的 RemoteSystemInfo 來請求加入:<br />
<pre class="code prettyprint"><code class="csharp-language">public async void OnJoinSessionSelectChangedAsync(object sender, SelectionChangedEventArgs e)
{
var listView = sender as ListView;
var session = listView.SelectedItem as RemoteSystemSessionInfo;
if (session!= null)
{
// 請求加入 session
var result = await session.JoinAsync();
Debug.WriteLine($"join {session.DisplayName}: {result.Status.ToString()}");
if (result.Status == RemoteSystemSessionJoinStatus.Success)
{
this.currentSession = result.Session;
// 注冊訊息通道做資料傳遞
RegistMessageChannel(currentSession, currentSession.DisplayName);
// 註冊有哪些參與者加入或離開
SubscribeParticipantWatcher(currentSession);
}
}
}</code></pre><br />
<h2>* 註冊 Session 的交易資料通道來發送或接收訊息</h2><pre class="code prettyprint"><code class="csharp-language">private RemoteSystemSessionMessageChannel appMessageChannel;
private void RegistMessageChannel(RemoteSystemSession session, string channelName)
{
if (appMessageChannel != null)
{
appMessageChannel.ValueSetReceived -= AppMessageChannel_ValueSetReceived;
appMessageChannel = null;
}
// 利用 RemoteSystemSession 注冊訊息通道
appMessageChannel = new RemoteSystemSessionMessageChannel(session, channelName);
appMessageChannel.ValueSetReceived += AppMessageChannel_ValueSetReceived;
}
private void AppMessageChannel_ValueSetReceived(RemoteSystemSessionMessageChannel sender, RemoteSystemSessionValueSetReceivedEventArgs args)
{
// 處理收到的訊息
ValueSet receivedMessage = args.Message;
if (receivedMessage != null)
{
// 建立一個假的 MessageData 類別來做為交易訊息的内容
MessageData msgData = new MessageData();
byte[] data = receivedMessage["Key"] as byte[];
using (MemoryStream ms = new MemoryStream(data))
{
DataContractJsonSerializer ser = new DataContractJsonSerializer(msgData.GetType());
msgData = ser.ReadObject(ms) as MessageData;
}
}
}</code></pre>要在 session 裏面傳遞訊息要記得註冊 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionmessagechannel" target="_blank">RemoteSystemSessionMessageChannel</a>。<br />
<br />
發送訊息的範例程式如下:<br />
<pre class="code prettyprint"><code class="csharp-language">public void OnSendMessageToSessionClick(object sender, RoutedEventArgs e)
{
// 準備要發送的訊息内容
var msg = new MessageData
{
Content = "test"
};
using (var stream = new MemoryStream())
{
new DataContractJsonSerializer(message.GetType()).WriteObject(stream, message);
byte[] data = stream.ToArray();
// 將内容包裝到 ValueSet
ValueSet sentMessage = new ValueSet { ["Key"] = data };
// 可以選擇發給全部的參與者或是特定的參與者們
await appMessageChannel.BroadcastValueSetAsync(sentMessage);
}
}
public class MessageData
{
public string Content { get; set; }
}
</code></pre>如果想要發訊息給特定的參與者,可以參考:<br />
<pre class="code printpertty"><code class="csharp-language">private RemoteSystemSessionParticipantWatcher participantWatcher;
public ObservableCollection<remotesystemsessionparticipant> Participants => watchedParticipants;
private ObservableCollection<remotesystemsessionparticipant> watchedParticipants;
private void SubscribeParticipantWatcher(RemoteSystemSession session)
{
if (participantWatcher != null)
{
participantWatcher.Stop();
}
participantWatcher = null;
// 當加入或建立 RemoteSystemSession 之後,利用 ParticipantWatcher 來看有那些參與者
participantWatcher = session.CreateParticipantWatcher();
participantWatcher.Added += (s, a) => {
watchedParticipants.Add(a.Participant);
};
participantWatcher.Removed += (s, a) => {
watchedParticipants.Remove(a.Participant);
};
participantWatcher.Start();
}</code></pre><br />
上面的範例可以到 <a href="https://github.com/poumason/DotblogsSampleCode/tree/master/DotblogsSampleCode/26-RemoteSystemSample" target="_blank">26-RemoteSystemSample</a> 下載來使用。<br />
<br />
幾個重要的元素:<br />
<ol><li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioncontroller" target="_blank">RemoteSystemSessionController</a><br />
負責建立與管理 new remote session 讓其他設備可以加入。每一個 session 都有一位參與者扮演管理者角色,只有管理者才能設定 session 的特性,允許誰可以加入,或是把人踢出。<table border="1"><tr><td><b>Constructors</b></td><td>RemoteSystemSessionController(String, <br />
RemoteSystemSessionOptions)</td><td>給與 remote session 的公開名稱,<br />
並設定相關特性 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionoptions" target="_blank">RemoteSystemSessionOptions</a></td></tr>
<tr><td><b>Methods</b></td><td>CreateSessionAsync()</td><td>非同步建立 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsession" target="_blank">RemoteSystemSession</a>。</td></tr>
<tr><td></td><td>RemoveParticipantAsync(<br />
RemoteSystemSessionParticipant)</td><td>從 Session 移除特定參與者(<a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionparticipant" target="_blank">RemoteSystemSessionParticipant</a>)。</td></tr>
<tr><td><b>Events</b></td><td>JoinRequested</td><td>當有另一個設備找到該 Session 並請求加入時會觸發。</td></tr>
</table></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionwatcher" target="_blank">RemoteSystemSessionWatcher</a><div>監視與發現遠端會話相關的活動並引發相應的事件。<br />
<table border="1"><tr><td>Status</td><td>取得 RemoteSystemSessionWatcher 的運作狀態</td></tr>
<tr><td>Added</td><td>當發現有新的 Session 被找到時會觸發</td></tr>
<tr><td>Removed</td><td>當之前被找到的 Session 消失時會觸發</td></tr>
<tr><td>Updated</td><td>當先前被找到的 Session 有部分資訊被更新時會觸發</td></tr>
</table></div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsession" target="_blank">RemoteSystemSession</a><div>表示和處理可在兩個或多個連接的設備之間共用的遠端會話。<br />
它是更廣泛的遠端系統功能集的一部分,它被建立之後可以讓多個設備加入並裏面交換訊息,實現跨裝置的資訊交換。<br />
<table border="1"><tr><td>CreateParticipantWatcher()</td><td>初始化 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionparticipantwatcher" target="_blank">RemoteSystemSessionParticipantWatcher</a> 來監看裏面的參與者</td></tr>
<tr><td>CreateWatcher()</td><td>實例化取得 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionwatcher" target="_blank">RemoteSystemSessionWatcher</a> 來監控可能的 session</td></tr>
<tr><td>SendInvitationAsync(RemoteSystem)</td><td>邀請某個 RemoteSystem 加入 Session。<br />
接受邀請的設備需要搭配 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninvitationlistener" target="_blank">RemoteSystemSessionInvitationListener</a> 來處理。</td></tr>
<tr><td>Disconnected</td><td>當這個設備從這個 remote session 斷線時被觸發</td></tr>
</table></div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninfo" target="_blank">RemoteSystemSessionInfo</a><div>内容包含 Remote Session 的相關資訊。它與 RemoteSystemSession 最大差別在於,已經加入的 session 用 RemoteSystemSession 代表,未加入的 session 則是 RemoteSystemSessionInfo (因爲它才有 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessioninfo.joinasync#Windows_System_RemoteSystems_RemoteSystemSessionInfo_JoinAsync" target="_blank">JoinAsync</a> 的 method 可以用)。</div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionparticipantwatcher" target="_blank">RemoteSystemSessionParticipantWatcher</a><div>負責監控 remote session 有哪些參與者加入/離開。</div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemsessionmessagechannel" target="_blank">RemoteSystemSessionMessageChannel</a><div>在 remote session 中處理訊息專用的資料傳輸通道,包括:傳送與接收。<table border="1"><tr><td>BroadcastValueSetAsync(ValueSet)</td><td>傳送訊息給所有的參與者</td></tr>
<tr><td>SendValueSetAsync(ValueSet, RemoteSystemSessionParticipant)</td><td>傳送訊息給特定的一位參與者</td></tr>
<tr><td>SendValueSetToParticipantsAsync(ValueSet, IIterable<remotesystemsessionparticipant>)</td><td>傳訊訊息給特定一群參與者</td></tr>
<tr><td>ValueSetReceived</td><td>當收到訊息時會觸發該事件</td></tr>
</table></div></li>
</ol>======<br />
另外 Windows Holographic 利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.perception.spatial.spatialentitystore" target="_blank">SpatialEntityStore</a> 建立多個設備之間訊息的傳遞與資料管理,更多細範例可參考 <a href="https://github.com/microsoft/Windows-appsample-remote-system-sessions" target="_blank">Quiz Game sample app</a>。<br />
這篇内容參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/remote-sessions
" target="_blank">Connect devices through remote sessions</a> 來加以説明,希望對大家有所幫助。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://github.com/Microsoft/Windows-appsample-remote-system-sessions" target="_blank">Windows-appsample-remote-system-sessions </a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/remote-sessions#share-messages-and-data-through-a-remote-session" target="_blank">Connect devices through remote sessions</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/connected-apps-and-devices" target="_blank">Connected apps and devices (Project Rome)</a></li>
<li><a href="https://blog.pieeatingninjas.be/2017/08/13/creating-a-continue-watching-experience-using-project-rome-in-uwp/" target="_blank">Creating a ‘Continue Watching’ experience using Project Rome in UWP</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-47351519629316313312018-07-21T10:57:00.001+08:002018-07-21T10:57:51.634+08:00UWP - 介紹 Project Rome - 1<a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/connected-apps-and-devices" target="_blank">Project Rome</a> 從 //Build 2016 發表的技術,讓 App 可以在同一個 Microsoft account (MSA) 的不同設備(Windows, Android, iOS)互相溝通。<br />
這一篇將介紹如何操作 <a href="https://msdn.microsoft.com/library/windows/apps/Windows.System.RemoteSystems" target="_blank">Remote Systems APIs</a> 做到這些應用找尋設備,啓動遠端設備中的 App 與 App Service。<br />
<a name='more'></a><br />
<b>重點提醒</b><br />
<ul><li>在 Windows 10, Version 1607 開始 <a href="https://msdn.microsoft.com/library/windows/apps/Windows.System.RemoteSystems" target="_blank">Remote Systems APIs</a> 支援讓 App 從一個設備開始處理任務,最後到另一臺設備的完成它。</li>
<li>設備能經由 bluetooth, wireless 或是 cloud (則需要相同的 Microsoft account (MSA) 連結)</li>
<li>常見的情境:<ul><li>用戶可能在車上用手機聼歌曲,等他回到家裏就可以直接把目前播放的進度改交給 Xbox One 繼續播放</li>
<li>利用 App Service 建立 channel 讓兩個設備直接互相溝通或控制</li>
</ul></li>
<li>開發的專案要宣告 RemoteSystem 的 capability:<pre class="code prettyprint"><code class="xml"><Capabilities>
<Capability Name="internetClient" />
<uap3:Capability Name="remoteSystem" />
</Capabilities></code></pre></li>
<li>如果要允許不同帳號也可以控制你的設備 (使用 <span class="inline-code">RemoteSystemAuthorizationKind.Anonymous</span>),需要開 <span class="inline-code">跨裝置體驗</span> 的設定,如下圖:<br />
<span id="docs-internal-guid-9b787210-9bdf-e175-c901-f143bd985f05"><span style="font-family: Arial; font-size: 11pt; font-variant-east-asian: normal; font-variant-numeric: normal; vertical-align: baseline; white-space: pre-wrap;"></span></span><span id="docs-internal-guid-229ef4ad-9be0-29c8-fe8c-fab51a4f68db"><span style="font-family: Arial; font-size: 11pt; font-variant-east-asian: normal; font-variant-numeric: normal; vertical-align: baseline; white-space: pre-wrap;"><img height="487" src="https://lh3.googleusercontent.com/yDK-5Yvyut5_5fVO5Co1wB9ic3E-AnxttlbPXzG_VXgWFXHr-5BIY8RZ49H1cVo9VmusPxWHMuR5pifPveBS703nqetI0h0tb2SQ_GHZedcXV1FgI4shpnq14zCjAkEClr7O-F7J" style="-webkit-transform: rotate(0.00rad); border: none; transform: rotate(0.00rad);" width="643" /></span></span></li>
</ul>有了觀念之後,下面介紹細部的開發。<br />
<br />
<h3>* 如何找到遠端設備</h3><b>1. 利用 <a href="https://msdn.microsoft.com/library/windows/apps/Windows.System.RemoteSystems.RemoteSystemWatcher" target="_blank">RemoteSystemWatcher</a> 找出設備:</b><br />
<a href="https://msdn.microsoft.com/library/windows/apps/Windows.System.RemoteSystems.RemoteSystemWatcher" target="_blank">RemoteSystemWatcher</a> 搭配 Filter 找出符合的設備,Filter 可以指定:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemdiscoverytype" target="_blank">RemoteSystemDiscoveryType</a>:proximal, local network, cloud connection</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemkinds" target="_blank">RemoteSystemKinds</a>:desktop, mobile, Xbox, Hub, Holographic 等</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemstatus" target="_blank">RemoteSystemStatus</a>:設備目前的狀態,例如:Available, DiscoveringAvailability, Unavailable, Unknown</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemplatform" target="_blank">RemoteSystemPlatform</a>:設備的作業系統類型,例如:Android, iOS, Windows, Linux 等</li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemauthorizationkind" targetr="_blank">RemoteSystemAuthorizationKind</a>:設定找尋到設備是否為相同的帳號或是任何設備;如果是不同用戶,必須是狀態為 available 且經由 proximal connection 方式的才能使用;如果沒有特別設定 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemauthorizationkindfilter" target="_blank">RemoteSystemAuthorizationKindFilter</a> 的話,<b>預設值為 same-user</b> 的設備才能被找到。</li>
</ul><pre class="code prettyprint"><code class="csharp-language">private async void OnStartDiscoverClick(object sender, RoutedEventArgs e)
{
// 要使用 RemoteSystem 前,需要先要求用戶給與權限
var requestPermission = await RemoteSystem.RequestAccessAsync();
if (requestPermission != RemoteSystemAccessStatus.Allowed)
{
return;
}
OnStopDiscoverClick(sender, e);
DiscoverDevicesAsync();
}
private RemoteSystemWatcher devicesWatcher;
private ObservableCollection<remotesystem> devicesList;
private List<iremotesystemfilter> GetRemoteSystemFilter()
{
List<iremotesystemfilter> filters = new List<iremotesystemfilter>();
// 設定要用什麽方式找設備, 利用 Any 比較多設備可以被找到
filters.Add(new RemoteSystemDiscoveryTypeFilter(RemoteSystemDiscoveryType.Any));
// 設定找到的設備要是什麽狀態
filters.Add(new RemoteSystemStatusTypeFilter(RemoteSystemStatusType.Available));
// 設定要找尋的設備類型
filters.Add(new RemoteSystemKindFilter(new List<string>
{
RemoteSystemKinds.Desktop, RemoteSystemKinds.Laptop, RemoteSystemKinds.Tablet,
RemoteSystemKinds.Phone,
RemoteSystemKinds.Xbox
}));
// 設定是否需要驗證的設備, 如果要相同帳號可以選 SameUser,預設是 SameUser
filters.Add(new RemoteSystemAuthorizationKindFilter(RemoteSystemAuthorizationKind.Anonymous));
return filters;
}
private void DiscoverDevicesAsync()
{
var filters = GetRemoteSystemFilter();
// Filters 需要在建立 RemoteSystemWatcher 建構子一起傳入
devicesWatcher = RemoteSystem.CreateWatcher(filters);
devicesWatcher.RemoteSystemAdded += DevicesWatcher_RemoteSystemAdded;
devicesWatcher.RemoteSystemRemoved += DevicesWatcher_RemoteSystemRemoved;
devicesWatcher.RemoteSystemUpdated += DevicesWatcher_RemoteSystemUpdated;
devicesWatcher.ErrorOccurred += DevicesWatcher_ErrorOccurred;
devicesWatcher.Start();
}
</code></pre><br />
[<b>注意</b>]<br />
<ul><li>RemoteSystemDiscoveryType 設定 proximal 不保證物理距離接近程度</li>
<li>如果需要可靠物理距離接近程度,可以使用 <span class="inline-code"><a href="https://docs.microsoft.com/uwp/api/windows.system.remotesystems.remotesystemdiscoverytype" target="_blank">RemoteSystemDiscoveryType.SpatiallyProximal</a></span>,但只有支援用藍牙找到設備</li>
<li>可利用 RemoteSystem 的 <a href="https://docs.microsoft.com/uwp/api/Windows.System.RemoteSystems.RemoteSystem.IsAvailableByProximity" target="_blank">RemoteSystem.IsAvailableBySpatialProximity</a> 確認被找到的設備是否在物理接近範圍內</li>
<li>RemoteSystemDiscoveryType 設定 local network 時,使用的網路 Profile 是 <span class="inline-code">private</span> 與 <span class="inline-code">domin</span>,設備不會在 <span class="inline-code">public</span> 被找到</li>
<li>如果設定 <span class="inline-code">RemoteSystemAuthorizationKind.Anonymous</span> 為過濾調整,只能在 proximal 範圍找到設備</li>
</ul><br />
<b>2. 利用 IP Address 找到設備</b><br />
<pre class="code prettyprint"><code class="csharp-language">private async Task<remotesystem> GetRemoteSystemByIp(string ipAddress)
{
HostName host = new HostName(ipAddress);
// 利用 IP Address 去找設備,如果找不到設備會拿到 null。
return await RemoteSystem.FindByHostNameAsync(host);
}</code></pre><br />
<h2>* 使用 <a href="https://www.blogger.com/blogger.g?tab=mj&blogID=2649688415868412622" target="_blank">RemoteSystem</a> 連線設備,並啓動特定的 URI</h2><pre class="code prettyprint"><code class="csharp-language">private async void ListView_ItemClick(object sender, ItemClickEventArgs e)
{
var remoteSystem = e.ClickedItem as RemoteSystem;
// 檢查設備是否支援需要的特性
// KnownRemoteSystemCapabilities.AppService 與 KnownRemoteSystemCapabilities.LaunchUri
var appService = await remoteSystem.GetCapabilitySupportedAsync(KnownRemoteSystemCapabilities.AppService);
var launchUri = await remoteSystem.GetCapabilitySupportedAsync(KnownRemoteSystemCapabilities.LaunchUri);
if (launchUri)
{
var launchRequest = new RemoteSystemConnectionRequest(remoteSystem);
var result = await RemoteLauncher.LaunchUriAsync(launchRequest, new Uri("https://poumason.blogspot.com"));
Debug.WriteLine(result.ToString());
}
}</code></pre>利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.knownremotesystemcapabilities" target="_blank">KnownRemoteSystemCapabilities</a> 定義了支援那些 Capabilities 的名稱,幫助開發人員確認 RemoteSystem 能支援什麽。<br />
再建立 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemconnectionrequest" target="_blank">RemoteSystemConnectionRequest</a> 物件交給 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotelauncher" target="_blank">RemoteLauncher</a> 要求 RemoteSystem 執行任務。<br />
<br />
<h2>* 讓 App Service 支援從另一個設備來呼叫:</h2>任何 Windows-based 的設備都可以當作 client 或是 host,表示 App Service 可以被安裝在任何一個角色來與對方互動。<br />
由於 App Service 的 UI-less 的特性,在呼叫遠端設備中的 App Service 時就無需將應用程式帶到 foreground。<br />
<br />
一樣使用 host 與 client 兩個角色來説明:<br />
<ol><li>host App 需要修改 Package.manifest 支援 <span class="inline-code">SupportsRemoteSystems</span>:<div><pre class="code prettyprint"><code class="xml"><Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
IgnorableNamespaces="uap mp uap">
<Applications>
<Application>
<Extensions>
<uap3:Extension Category="windows.appService" EntryPoint="MyAppService.ServiceTask">
<uap3:AppService Name="com.pou.MyApService" SupportsRemoteSystems="true" />
</uap3:Extension>
</Extensions>
</Application>
</Applications>
</Package></code></pre><span class="inline-code">SupportsRemoteSystems</span> 是新的 xmlns,所以要記得加入 uap3 的宣告。</div></li>
<li>host App 的 App Service 加入處理來自 client 的請求,以 <a href="http://poumason.blogspot.com/2018/07/app-service_14.html" target="_blank">UWP - 介紹 App Service 與新功能</a> 的範例爲主<div></div></li>
<li>client App 利用 <span class="inline-code">Remote Systems</span> 呼叫遠端設備的 App Service:<div><pre class="code prettyprint"><code class="csharp-language">AppServiceConnection connection = new AppServiceConnection();
connection.AppServiceName = "com.pou.MyApService";
connection.PackageFamilyName = "f9842749-e4c8-4c15-bac8-bc018db1b2ea_s1mb6h805jdtj";
RemoteSystemConnectionRequest appServiceRequest = new RemoteSystemConnectionRequest(remoteSystem);
AppServiceConnectionStatus status = await connection.OpenRemoteAsync(appServiceRequest);
if (status == AppServiceConnectionStatus.Success)
{
var message = new ValueSet();
message.Add("cmd", "Query");
message.Add("id", "1234");
AppServiceResponse response = await connection.SendMessageAsync(message);
if (response.Status == AppServiceResponseStatus.Success)
{
if (response.Message["status"] as string == "OK")
{
Debug.WriteLine(response.Message["name"] as string);
}
}
}
</code></pre>同樣利用 RemoteSystem 建立 RemoteSystemConnectionRequest,搭配 <a href="https://msdn.microsoft.com/library/windows/apps/windows.applicationmodel.appservice.appserviceconnection.aspx" target="_blank">AppServiceConnection</a> 建立連線,再把訊息送到遠端設備的 App Service 來獲取結果。</div></li>
</ol>上面的範例可以到 <a href="https://github.com/poumason/DotblogsSampleCode/tree/master/DotblogsSampleCode/26-RemoteSystemSample" target="_blank">26-RemoteSystemSample</a> 下載來使用。<br />
<br />
幾個重要的元素:<br />
<ol><li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystem" target="_blank">RemoteSystems</a><div>該類別管理被找到的遠端設備屬性與它可支援的特性。重要的内容:<br />
<table border="1"><tr><td><b>Type</b></td><td><b>Name</b></td><td><b>Description</b></td></tr>
<tr><td>Properties</td><td>IsAvailableByProximity</td><td>利用 proximal connection (近距離連線,例如:Bluetooth, <br />
local network connection)檢查給定的設備是否可以用,而不是透過雲端連線。</td></tr>
<tr><td></td><td>IsAvailableBySpatialProximity</td><td>通過空間近距離的連接檢查給定的遠端系統是否可用。</td></tr>
<tr><td>Methods</td><td>CreateWatcher(IIterable<iremotesystemfilter>)</td><td>搭配設定 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.iremotesystemfilter" target="_blank">RemoteSystemFilter</a> 的條件,<br />
建立 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemwatcher" target="_blank">RemoteSystemWatcher</a> 來搜尋 Remote Systems。</td></tr>
<tr><td></td><td>FindByHostNameAsync(HostName)</td><td>企圖找到特定 IP Address 或 Host Name 的設備</td></tr>
<tr><td></td><td>GetCapabilitySupportedAsync(String)</td><td>檢查該 RemoteSystem 是否有支援特定功能,<br />
搭配 <a href=“https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.knownremotesystemcapabilities" target="_blank">KnownRemoteSystemCapabilities</a> 使用。</td></tr>
<tr><td></td><td><b>RequestAccessAsync()</b></td><td>每次在使用 RemoteSystem 之前一定要呼叫,<br />
讓用戶允許 App 有權限使用相關特性。</td></tr>
</table></div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems.remotesystemconnectionrequest" target="_blank">RemoteSystemConnectionRequest</a><div>代表與特定設備連線溝通的意圖。例如:要求 RemoteSystem 執行 remote launch 或 remote app service 都需要利用它來建立連線。</div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotelauncher" target="_blank">RemoteLauncher</a><div>啟動與遠端設備上的指定 URI 關聯的預設應用程式。<br />
常見都是設定 URI,如果需要比較複雜的使用可以參考 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotelauncher.launchuriasync#Windows_System_RemoteLauncher_LaunchUriAsync_Windows_System_RemoteSystems_RemoteSystemConnectionRequest_Windows_Foundation_Uri_Windows_System_RemoteLauncherOptions_Windows_Foundation_Collections_ValueSet_" target="_blank">LaunchUriAsync(RemoteSystemConnectionRequest, Uri, RemoteLauncherOptions)</a>:可以設定 FallbackUri 或 PreferredAppIds (給于package family names) 啓動指定的 App。</div></li>
</ol>詳細説明可以參考 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.system.remotesystems" target="_blank">Windows.System.RemoteSystems Namespace </a>,或是範例 <a href="https://github.com/Microsoft/Windows-universal-samples/tree/dev/Samples/RemoteSystems" target="_blank">Remote Systems UWP sample</a>。<br />
======<br />
這篇的介紹是以 Windows 設備為主,如果想要 Windows 操作其他設備,可以參考微軟提供的 SDK:<a href="https://blogs.windows.com/buildingapps/2017/02/08/announcing-project-rome-android-sdk/" target="_blank">Announcing Project Rome Android SDK</a> 與 <a href="https://blogs.windows.com/buildingapps/2017/05/16/announcing-project-rome-ios-sdk/" target="_blank">Announcing Project Rome iOS SDK</a>。<br />
希望對大家有所幫助。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://developer.microsoft.com/en-us/windows/project-rome" target="_blank">Project Rome</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/02/08/announcing-project-rome-android-sdk/" target="_blank">Announcing Project Rome Android SDK</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/05/16/announcing-project-rome-ios-sdk/" target="_blank">Announcing Project Rome iOS SDK</a></li>
<li><a href="https://github.com/Microsoft/project-rome" target="_blank">Microsoft/project-rome</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/RemoteSystems" target="_blank">Remote Systems sample</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/AppServices" target="_blank">App services sample</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/JumpList" target="_blank">Jump list customization sample</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/connected-apps-and-devices" target="_blank">Connected apps and devices (Project Rome)</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/discover-remote-devices" target="_blank">Discover remote devices</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/launch-a-remote-app" target="_blank">Launch an app on a remote device</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/communicate-with-a-remote-app-service" target="_blank">Communicate with a remote app service</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/remote-sessions" target="_blank">Connect devices through remote sessions</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/dev/Samples/RemoteSystems" target="_blank">Remote Systems sample</a></li>
<li><a href="https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-appmodel-v2-overview" target="_blank">Sign-in Microsoft Account & Azure AD users in a single app</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2016/10/11/cross-device-experience-with-project-rome/#6dqS4AvibQcbGcV7.97" target="_blank">Cross-device experiences with Project Rome</a></li>
<li><a href="https://github.com/Microsoft/project-rome/tree/master/Project%20Rome%20for%20Android%20(preview%20release)" target="_blank">Project Rome for Android (preview release)</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/02/08/announcing-project-rome-android-sdk/#bsIHEOluSp3AJdzz.97" target="_blank">Announcing Project Rome Android SDK</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">建立和取用 App 服務</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-31938027109348665862018-07-14T12:44:00.000+08:002018-07-14T12:44:52.440+08:00介紹 //Build 2018 公佈在 Microsoft Store 與 Dev Center加入的新特性本篇内容介紹 //Build 2018 在 <a href="https://channel9.msdn.com/Events/Build/2018/BRK2415" target="_blank">Microsoft Store and Dev Center: Updated and new features to help you be successful</a> 提到調整分潤,新的 App 安裝機制,如何幫助開發者收入,<a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">PWA</a>等新的特性。<br />
<a name='more'></a><br />
<ul><li><b>新的分潤模式</b><div><img height="323" src="https://lh5.googleusercontent.com/hXAx2jS1tvG1TAsQtn2S025HvpYx-Gz0tGgB3pVkk5yhwvVC681iH05-9Hf8ji19JWpcyou7UEf3E0UYgnrqocnVsBOIyDK5USV7jSDaxr7pxVnDKqXyZmqk50eOHsZL5KYjVJ53" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul type="circle"><li>95%:用戶透過 deep link (custom url, protocol, ms store link) 進去 Store 網站或是 App 内購買 App 或 in-app products。</li>
<li>85%:微軟幫助用戶從其他 marketing channel (facebook, blogger, Ad, etcs) 找到 App 並購買。</li>
</ul></div></li>
<li><b>改善安裝 Apps 的入口</b><div><img height="348" src="https://lh4.googleusercontent.com/X9JGSjf0fd6A74mV3-46iCArdpOPSrbQPmO2tSN6N0krgolflMcs2i2RjqqTXzYd1E2pEPMVswKslTS3pMkdCAyDGWH2vt159Xyp1jhdXy1ref60sHmsJ5O3y802Zqt9jD7RjL4a" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
提供從 Microsoft web store (new) 直接購買 App / add-on,並選擇要安裝到哪一個已經註冊的設備(相似 Google Play 機制,稱爲 Push-to-Install (PTI))。</div></li>
<li><b>改善用戶對 App 寫評論的機制</b><div><img height="349" src="https://lh6.googleusercontent.com/4FVT-7eWoYKpYZ55QUsLeY01wPVgTGs9HRln18kiIdALo0T6PhKREn2DMua6-CbIIlNyaIonjebmM8mb5b0SwMBOL-42ogbnk2Jweq2MQZIxKEyhVp0mT6my6twNzePYn5_wdAI6" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>改善 App Review 的方式,之前是開到 Store 改為直接在 App 裏面 (in-app review),讓用戶隨時都可以留下評論。</li>
<li>Case studying 發現這樣調整,用戶每天會在 App 裏面給 Review 有 70 %,超過 10 次以上會調整 rating。</li>
<li><span class="inline-code">StoreRequestHelper.SendRequestAsync(StoreContext.GetDefault(), 16, string.Empty);</span>,16 代表告訴 Store API 要用 in-app review</li>
</ul></div></li>
<li><b>利用 <a href="https://github.com/Microsoft/msix-packaging" target="_blank">MSIX</a> 作爲新的發佈機制</b><div><img height="363" src="https://lh4.googleusercontent.com/iRm4pxE9HCaU16mA2HWElcqjRHeAjkR3AHjPo59QBTmC5zCcZ8BXSqQtas9ASAVXhR6twJNDCfr_viTJTZtKrVYGuSYWkeZPmWd8eq1eY-fdxtXYy1TAqeRO2cD-D4BiK_3XRdss" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>可搭配 MSIX 機制增加新的用戶(企業用戶),因爲企業用戶不一定會開放 Store 下載,搭配 MSIX 可以裝 Appx/Win32.exe 進行安裝。</li>
<li>MSIX 更支援部分模組安裝或是更新,相當方便。</li>
<li>MSIX 還支援 Windows 7 哦!</li>
</ul></div></li>
<li><b>改善 App 效能並讓 Windows 10 支援 ARM 架構</b><div><img height="327" src="https://lh3.googleusercontent.com/Yx242dwH0NNEETttwEN5-o7rw7JCyIoY0W8pjyRKu88BKzwmOi3Jkc9K01dTzNk7DzEQgdNMxtQDxOZNuvzUDf9Rm9HvhgtZqt9dHw4jVDONLPns35pKg7JvnG0f2D0Kdu50UfLR" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>期待 Windows 10 on ARM x86/x64 的完成,讓 App 開發的時候可以增加選擇 ARM x64 。搭配 VS 15.8 preview 可以選擇 ARM x64 的 platform 來測試。</li>
<li>可以參考 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/porting/apps-on-arm" target="_blank">ARM 上的 Windows 10</a> 介紹在 ARM 上面開發的注意事項。</li>
<li>如果您開發的 UWP App 不需要做調整就可以直接運行在 ARM 架構;但如果是 x86 的 Win32 App,Windows 10 會用 <a href="https://docs.microsoft.com/en-us/windows/uwp/porting/apps-on-arm-x86-emulation" target="_blank">x86 emulation</a> 機制讓 App 可以在 ARM 架構下運作。</li>
</ul></div></li>
<li><b>開始支援與推廣 <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">Progressive Web Apps (PWA)</a></b><div><img height="348" src="https://lh6.googleusercontent.com/wsTjb7rL7FIMBTSX2w16GsrE_P87hZj-S0vXhUduVvHUCKnURpOj2Ywk0aVhqItyn4z_0DXZd9jIEvi0tRLVEmQQJDz24fqyzMVBR4erMlN-odIxjRtqoaqebEH_ZjypAdzQCAAA" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>MS Store 支援上架 PWA 的 App,可以參考 <a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">Progressive Web Apps on Windows</a> 瞭解開發重點。</li>
<li>目前開放支援的 local services 與 APIs (push notification 或 local data),接近 native app-like experience。</li>
<li>推出 <a href="https://github.com/pwa-builder" target="_blank">PWABuilder</a> 可幫忙把現有的 Web 建立 PWA 程式並上架,非常有效果。</li>
<li>目前 Twitter, Uber, Starbucks, Pinterest 等應用城市都是使用 <a href="https://github.com/pwa-builder" target="_blank">PWABuilder</a> 建立</li>
</ul></div></li>
<li><b>Dev Center 提供更完整的 Private Audience</b><div><img height="351" src="https://lh5.googleusercontent.com/fzeS6GN7oXqsGDe79myWI7jJhDMT8DYfKejonLlMKLnrrOEt9G5EsE4ByFyyzmbG6yfrd-KC-bqCHHEGhgfsN4RVCzOJ1Wq2YBPLiJbFwUbeaytUIjL7eQenY_HYguUoZcXZAW01" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>提供 private audience 提前讓部分用戶可以先安裝與測試,不會外流到外部,可以加入排程統一時間發佈所有用戶都能看到。</li>
<li>除了 App 之外,Add-ones 的產品也支援,但是我自己測試的時候發現 Microsoft web store 測試還沒有很完整,在 Store App 是可以的。</li>
</ul></div></li>
<li><b>建議多利用 Hero trailers 幫助用戶更快瞭解 App 的特性,增加下載或購買機會</b><div><img height="356" src="https://lh6.googleusercontent.com/TJIxScfAwtChu8IVSbLHgdXuvDKGltJFsl6jiHjlwBbv1TQI9rqNw7kxRyTeHDkHpDdEBwijTjSL_-dzxcs4DA2V38IN2_VlNiqD-m9xnlqZmxjluURF_0bA_Far5p0BC9W65BbV" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
上架 App 在填寫介紹内容時,有一塊 Hero 區域,建議可以使用,裏面可以放圖片或是影片,它會用戶瀏覽您的 App 時播放介紹的影片與圖片,讓用戶更快瞭解您的 App。這個很常用在遊戲產品,不妨嘗試看看。</div></li>
<li><b>大量改善上架時繁瑣的填寫項目與加快審核速度</b><div><img height="271" src="https://lh4.googleusercontent.com/A2BkH0mEbuTeSCiPrQjEheX0mbGdRkRRP49vmGl86qd0QPCzMs5M78fMh5ZtkUi5Uq5eUUrvZa-8d-dvKV83Z9zul1Wk56NcxWhhaeV2MUUsp7V9aPAlAIxvbWVRp0BGfBUI-eXW" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<a href="https://developer.microsoft.com/en-us/store/insider-program" target="_blank">Dev Center 在新的 Preview 版本</a>改善幾個重點:<br />
<ul><li>定價細分到每個國家 (之前有,但是很難設定)</li>
<li>每一個產品的 submission 都拉在主要畫面顯示</li>
<li>誰調整了什麽設定與 submission 都有 log 記錄</li>
<li>簡化填寫上架資訊時需要不斷切換畫面的等待時間,集合相似的設定在同一個畫面</li>
<li>驗證 App 利用 machine learning 的做法加快驗證,減少 scrutiny。超過 70% 的 submission 利用這個機制進行審核</li>
<li>支援模組化提交,但 App 需要先符合支援模組化架構</li>
</ul></div></li>
<li><b>Dev Center 增加更多分析資訊的維度</b><div><img height="260" src="https://lh3.googleusercontent.com/tv66R9aF219mbSTZ4T6ETOPRz5E9UOr28Oeca96RZHxLZuo_ctOXPwursOm0rL37O6K9jovS62hZ02L9nn-1q0NtV2QjAMzJhg-_z0DCeiwkVE4y2mswrZDbWoY-vMsSX1xKgVHG" style="border-color: rgb(0, 0, 0); border-style: none; border-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><img height="337" src="https://lh4.googleusercontent.com/lnH8RUvSzoilkD2oRxjdvnA2XOMhI2qwkX8FN87TmrqjlPOKOKcBiH7k_ErkBpmvL9DjsgM7KtGt89aQBYPNwk_YD2s42Af6-y5VvQk4E1Cm9eMEBeC-HLOP4c3QlMAf9LLjXg1i" style="border: 0px none rgb(0, 0, 0); font-family: Arial; font-size: 14.66px; transform: matrix(1, 0, 0, 1, 0, 0); white-space: pre-wrap;" width="643" /><br />
<span id="docs-internal-guid-633705b5-970c-4df4-6fe4-f01bca3aaac6" style="font-family: "arial"; font-size: 14.66px; vertical-align: baseline; white-space: pre-wrap;"><img height="344" src="https://lh6.googleusercontent.com/wwd0IUjt0NJ2v5OO96RrSJSKsQlq0qMUcDQq60yL1bzL5znhUXc2iaTRBlp4OmAq-zHSRQ9IiqKurmibejYy2FDBbDdPIG-wIBE2YmYGhjnfT1ggyLhA37z8TglyKmAoLF8hRGcW" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /></span><br />
<ul><li>Deep dive on failure details<ul><li>crash 可以下載 cab file</li>
<li>上傳 symbol files 可以看到 call stakcs,在 Dev Center 做到像 HockeyApp 把相同的 crash 放在一起,統計 Crash 數量做有效的提醒開發人員處理</li>
<li>從 reviews 做分析,分類出那些跟效能有關係或是其他問題,方便 filter</li>
</ul></li>
<li>app health report 更明確定義出 crash 的類型,可以直接在 Dev Center 做 filter 查詢;每一種 insdier test package 也有自己的 health report</li>
<li>Store REST APIs 增加新的功能可以抓到詳細的資料</li>
<li>report 提供 engage users 幫助開發者瞭解用戶的問題,增加黏著度</li>
<li>可搭配 custom engagement 觀察特定的維度並發出通知</li>
<li>提供觀察 desktop application (win32 applications) 的數據</li>
<li>如果有開發 office add-ins 的應用,也可以在 Dev Center 上看到資料</li>
</ul></div></li>
<li><b>Improve ways to monetize apps and games</b><div><img height="212" src="https://lh3.googleusercontent.com/2LejSCcdQZTjheX0dBxiVpRme1EXdAQk4toHYZubeEbJbBxUOlPcbpSsSWMh6bgy6fZYlCQKkwCEwink-YHMAxROz5ZDwnqgtPB3SfrEwUaNeL5BBBFvdmfI4SdcjbMvJj7J9O6N" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
<ul><li>定價更多彈性:增加可以對特定地區,時間給與不同的定價,例如:節慶活動,促銷活動等。如下圖:<img height="333" src="https://lh3.googleusercontent.com/uUj3Ys9kXEZSNnix_Tkdenq_HV2mS5xtnyOzxNd2v5OMiAVtO3a74Pzi27HuUjZtqB8sGLU-86KNsPqPF5mD9kYHb4s8-S-PFD95y2xRd-MtKf34Hs5nVnrDublqp0T3gvCTjB9u" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /></li>
<li>支援訂閲模式:讓用戶轉到訂閲模式,訂閲模式增加多種類型的選擇;由 Store 負責管理 billing ,開發者可不需要而外處理</li>
<li>加强廣告機制:<ul><li>爲什麽建議使用廣告?<ul><li>Store 提供更多種類型的廣告:13 種 ad networks</li>
<li>由 Server-side 統一管理,Dev center 可以直接調整廣告的投放比例</li>
<li>ROI 計算也比過去來的高,幫助市場顯示</li>
</ul></li>
<li>revenue increase 讓開發人員可以領更多:<div><img height="241" src="https://lh5.googleusercontent.com/2abkgGy0wR4DRPFAnrmfEip5hCOpf8TiPsYMM-jBVbKmXsq97SnohfjDEFYZAZFTp4L-ZmN2AsoyklIz96JGxzDyQf6dEAqAURSydLnbVxB5RJd5o7lw5WTtqIDJicsDge0JV2_2" style="border: 0px none rgb(0, 0, 0); transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><img height="324" src="https://lh6.googleusercontent.com/cjT5i7be7a6ojhpkAywneRUIT5ew9uo5e38IovrMXoZom-TTBrA95cdayW-ktrlt_9_9Rem2L3IRDI1Z_xDPeRVST1K0O-39u8yvkMiOcAWxNoJS0i_RTi43WpJOtWSgc_cR8HPO" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /><br />
在 Dev Center 增加可以監控那些投放的廣告在顯示,點擊率的數據,提供開發者調整廣告的顯示(view ability report in Dev Center)</div></li>
<li>增加機制讓廣播投放效果更好:<img height="315" src="https://lh6.googleusercontent.com/_q31-ruM2yUTX2Jk84Tq1B9z56yt2JqLkpVdPGZ6CrBrf47Re7UP78S5TnaB8maAIC4CBDPh6uBoJBy8jSUiAHFOTF9NLosQ2_tiyaSMS1CBD2F-Te-irXER8BOg9B1UH6H1vR4P" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" />每一種廣告可以指定在那些對象顯示。<br />
廣告類型除了 banner ,更包含其他内型的多媒體類型,多媒體(如影片)拉高廣告點擊的次數,最終也影響銷售的統計。<br />
</li>
</ul></li>
</ul></div></li>
<li>如何建立更棒的 Apps,可以參考下圖的建議來持續改進:<img height="269" src="https://lh4.googleusercontent.com/e5UFT0WPS9zuJ8oC4vsv2LII33U7BIhVPiPZlLE7xizM_6VAPfCGvhj1AXBSLehZeeCD2lkHZe8R3pMpKFnYf0OxqxOgL5gEE3E0NMOTe1UswR7CsDCrkgHfp0idmGm8xOk0CcMN" style="border-bottom-color: rgb(0, 0, 0); border-bottom-style: none; border-bottom-width: 0px; border-image-outset: 0; border-image-repeat: stretch; border-image-slice: 100%; border-image-source: none; border-image-width: 1; border-left-color: rgb(0, 0, 0); border-left-style: none; border-left-width: 0px; border-right-color: rgb(0, 0, 0); border-right-style: none; border-right-width: 0px; border-top-color: rgb(0, 0, 0); border-top-style: none; border-top-width: 0px; transform: matrix(1, 0, 0, 1, 0, 0);" width="643" /></li>
</ul><br />
======<br />
以上内容希望有幫忙到想要投入或者是已經在 Microsoft Store 上架的開發者有更多賺錢的機會。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://blogs.windows.com/buildingapps/2018/05/07/a-new-microsoft-store-revenue-share-is-coming/" target="_blank">A new Microsoft Store revenue share is coming</a></li>
<li><a href="https://channel9.msdn.com/Events/Build/2018/BRK2415" target="_blank">Microsoft Store and Dev Center: Updated and new features to help you be successful</a></li>
<li><a href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps" target="_blank">Progressive Web Apps on Windows</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/blob/master/Samples/Store" target="_blank">Store API Samples</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/publish/using-the-windows-dev-center-dashboard" target="_blank">Using the Windows Dev Center dashboard</a></li>
<li><a href="https://developer.microsoft.com/en-us/windows/campaigns/windows-developer-day" target="_blank">Windows Developer Day</a></li>
<li><a href="https://kkbox.codes/archives/2018/07/12/build-2018-part2/#more-760" target="_blank">Build 2018 大會系列 Part 2: Windows Development</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-65940682674326438082018-07-14T00:36:00.002+08:002018-07-18T11:40:57.230+08:00UWP - 介紹 App Service 與新功能App Service 是一種背景工作運行的服務,提供給其他 Apps 使用就像 Web Service。它本身無使用介面(UI-less),允許 Apps 在同一個設備被引用,甚至 Windows 10 1607 開始允許 remote devices 使用它。<br />
<a name='more'></a><br />
[<b>重點觀念</b>]<br />
<ul><li>Windows 10, version 1607 開始, App Service 支持新模式:<br />
<ul><li>可以與 host App 運行在相同的 process;(一般屬於 Background Task 執行在不同的 process)</li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/communicate-with-a-remote-app-service" target="_blank">支援從 App 呼叫 Remote Devices 中的 App Service</a>;</li>
</ul></li>
<li>想要 App Service 每次被啓動都是新的 instance,在 Package.appmanifest 加入宣告;<span class="inline-code">uap4:SupportsMultipleInstances="true"</span>;但需要 Windows 10, version 15063 以上才支援</li>
<li>App Service 的生命周期,因爲 Process 有所不同:<br />
<ul><li>Background Task (out-of-process):<br />
<ul><li>當它被建立時會進入 <b>Run()</b>,隨著 <b>Run()</b> 執行完畢就會被結束</li>
<li>它被啓動後,基本會維持活著約有 30 秒,可搭配呼叫 <b>GetDeferral()</b> 多加 5 秒來完成任務</li>
</ul></li>
<li>In-app process model:生命周期則跟著呼叫者一起共存,讓兩個 Apps 之間更容易溝通,不用再分成兩份 code 來串聯與維護</li>
</ul></li>
<li>App Service 的 OnTaskCancel() 被觸發有幾個原因:<br />
<ol><li>Client app 釋放<a href="https://msdn.microsoft.com/library/windows/apps/windows.applicationmodel.appservice.appserviceconnection.aspx" target="_blank">AppServiceConnection</a></li>
<li>Client app 被 suspended</li>
<li>系統關閉或睡眠</li>
<li>系統執行該 Task 用過高的資源</li>
</ol></li>
</ul><br />
大略有概念之後,接著介紹怎麽做基本的 App Service (two process),再介紹怎麽整合到 App 的 process 裏面;<br />
<b>* 如何建立 App service 並使用它:</b><br />
<br />
分成兩個 App 做説明:一個是擁有 App Service 的 Host App;一個是使用 App Service 的 Client App;<br />
<ol><li>建立一個 Windows Runtime Component,並且加入AppServiceConnection 的處理邏輯:<br />
<div><pre class="code prettyprint"><code class="csharp-language">public sealed class ServiceTask : IBackgroundTask
{
private BackgroundTaskDeferral backgroundTaskDeferral;
private AppServiceConnection appServiceconnection;
public void Run(IBackgroundTaskInstance taskInstance)
{
// Background Task 被建立時,取得 deferral 拉長生命周期,避免被結束
this.backgroundTaskDeferral = taskInstance.GetDeferral();
// 一定要註冊處理 Canceled 事件來正確釋放用到的資源
taskInstance.Canceled += OnTaskCanceled;
// 根據被啓動的 Instance 類型,建立 App Service Connection,並註冊 Request 事件.
var details = taskInstance.TriggerDetails as AppServiceTriggerDetails;
appServiceconnection = details.AppServiceConnection;
appServiceconnection.RequestReceived += OnRequestReceived;
}
private void OnTaskCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
if (this.backgroundTaskDeferral != null)
{
// Complete the service deferral.
this.backgroundTaskDeferral.Complete();
}
}
private async void OnRequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
// 當 App Service 收到請求時,該 method 就會被觸發
// 先要求取得 取得 deferral 拉長生命周期
var requestDeferral = args.GetDeferral();
ValueSet message = args.Request.Message;
string cmd = message["cmd"] as string;
string id = message["id"] as string;
ValueSet responseMsg = new ValueSet();
switch (cmd)
{
case "Query":
responseMsg.Add("id", "123456");
responseMsg.Add("name", "pou");
responseMsg.Add("status", "OK");
var result = await args.Request.SendResponseAsync(responseMsg);
break;
}
requestDeferral.Complete();
}
}</code></pre></div></li>
<li>在 Host App 的 Package.manifest 宣告 App Service 並設定 Entry Point,記得把 App Service 的專案加入到 Host App 的專案參考:<br />
<div><pre class="code prettyprint"><code class="xml"><Applications>
<Application Id="App" Executable="$targetnametoken$.exe" EntryPoint="ServiceHost.App">
<uap:VisualElements />
<Extensions>
<uap:Extension Category="windows.appService" EntryPoint="MyAppService.ServiceTask">
<uap:AppService Name="com.pou.MyApService" />
</uap:Extension>
</Extensions>
</Application>
</Applications></code></pre>加入專案參考這樣在 Host App 被安裝的時候才會一并加入 App Service。 利用 <span class="inline-code">Windows.ApplicationModel.Package.Current.Id.FamilyName</span> 在 Host App 拿到 package family name,準備交給 Client App。</div></li>
<li>在 Client App 利用 <a href="https://msdn.microsoft.com/library/windows/apps/windows.applicationmodel.appservice.appserviceconnection.aspx" target="_blank">AppServiceConnection</a> 呼叫 App Service:<br />
<pre class="code prettyprint"><code class="csharp-language">private async void MainPage_Loaded(object sender, RoutedEventArgs e)
{
AppServiceConnection connection = new AppServiceConnection();
connection.AppServiceName = "com.pou.MyApService";
connection.PackageFamilyName = "f9842749-e4c8-4c15-bac8-bc018db1b2ea_s1mb6h805jdtj";
var status = await connection.OpenAsync();
if (status != AppServiceConnectionStatus.Success)
{
Debug.WriteLine("Failed to connect");
return;
}
var message = new ValueSet();
message.Add("cmd", "Query");
message.Add("id", "1234");
AppServiceResponse response = await connection.SendMessageAsync(message);
string result = "";
if (response.Status == AppServiceResponseStatus.Success)
{
if (response.Message["status"] as string == "OK")
{
result = response.Message["name"] as string;
}
}
}</code></pre></li>
</ol>上面介紹的 App Service 是比較一般的用法, 把 App Service 放到 Background Task 的架構。<br />
<br />
<b>* 把 App Service 合併到 App.xaml.cs 裏面,作爲 Same Process:</b><br />
<a href="https://msdn.microsoft.com/library/windows/apps/windows.applicationmodel.appservice.appserviceconnection.aspx" target="_blank">AppServiceConnection</a> 允許其他 App 叫醒在背景中自己的 App 並傳入指令。它與上方的 out-of-process 最大不同有兩個:<br />
<ol><li>Package.manifest 宣告 <span class="inline-code"><uap:Extension Category="windows.appService"></span> 不用 Entry Point,改用 <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.application.onbackgroundactivated.aspx" target="_blank">OnBackgroundActivated()</a>。<br />
<div><pre class="code prettyprint"><code class="xml"><Package>
<Applications>
<Application>
<Extensions>
<uap:Extension Category="windows.appService">
<uap:AppService Name="com.pou.MyApService" />
</uap:Extension>
</Extensions>
</Application>
</Applications></code></pre>OnBackgroundActivated 代表 App 在背景時被啓動,Life cycle 可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-lifecycle" target="_blank">Windows 10 universal Windows platform (UWP) app lifecycle</a>。</div></li>
<li>在 App.xaml.cs 加入 <b>OnBackgroundActivated()</b> 的處理邏輯。<br />
<div><pre class="code prettyprint"><code class="csharp-language">sealed partial class App : Application
{
private AppServiceConnection appServiceConnection;
private BackgroundTaskDeferral appServiceDeferral;
protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args)
{
base.OnBackgroundActivated(args);
AppServiceTriggerDetails appService = args.TaskInstance.TriggerDetails as AppServiceTriggerDetails;
if (appService ==null)
{
return;
}
args.TaskInstance.Canceled += OnAppServicesCanceled;
// appServiceDeferral 與 appServiceConnection 需要變成公用變數
// 因爲其他時間需要用到,已維持連線的一致性
appServiceDeferral = args.TaskInstance.GetDeferral();
appServiceConnection = appService.AppServiceConnection;
appServiceConnection.RequestReceived += AppServiceConnection_RequestReceived;
appServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed;
}
private async void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args)
{
// 當 App Service 收到請求時,該 method 就會被觸發
// 先要求取得 取得 deferral 拉長生命周期
var requestDeferral = args.GetDeferral();
ValueSet message = args.Request.Message;
string cmd = message["cmd"] as string;
string id = message["id"] as string;
ValueSet responseMsg = new ValueSet();
switch (cmd)
{
case "Query":
responseMsg.Add("id", "123456");
responseMsg.Add("name", "pou");
responseMsg.Add("status", "OK");
var result = await args.Request.SendResponseAsync(responseMsg);
break;
}
requestDeferral.Complete();
}
private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)
{
appServiceDeferral?.Complete();
appServiceConnection?.Dispose();
}
private void OnAppServicesCanceled(IBackgroundTaskInstance sender, BackgroundTaskCancellationReason reason)
{
appServiceDeferral?.Complete();
appServiceConnection?.Dispose();
}
}</code></pre></div></li>
</ol>要支援 in-process model 就是這樣簡單,而且讓原本的 App Service 邏輯回到 App 本身,讓邏輯更乾净。<br />
<span class="inline-code">OnBackgroundActivated()</span> 負責處理 App Service 的啓用,並儲存 Deferral 保持服務的生命周期。<br />
詳細可以參考 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/app-lifecycle#running-in-the-background" target="_blank">Windows 10 通用 Windows 平台 (UWP) app 週期</a>。<br />
<br />
介紹完怎麽實作之後,下面補充幾個重要的元件:<br />
<ul><li><b><a href="https://msdn.microsoft.com/library/windows/apps/windows.applicationmodel.appservice.appserviceconnection.aspx" target="_blank">AppServiceConnection</a></b><br />
<div>代表連線到 App Service 的端點,App Service 允許 UWP App 之間互相溝通。幾個重點:<br />
<table border="1"><tbody>
<tr> <td><b>Type</b></td> <td><b>Name</b></td> <td><b>Description</b></td> </tr>
<tr> <td>Properties</td> <td>AppServiceName</td> <td>取得或設定想要操作的 App Service Name</td> </tr>
<tr> <td> </td> <td>PackageFamilyName</td> <td>取得或設定該 App Service 所屬 package 的 family name</td> </tr>
<tr> <td>Methods</td> <td>Dispose()</td> <td>Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.</td> </tr>
<tr> <td> </td> <td>OpenAsync()</td> <td>Opens a connection to the endpoint for the app service.</td> </tr>
<tr> <td> </td> <td>OpenRemoteAsync(RemoteSystemConnectionRequest)</td> <td>Opens a connection to the endpoint on another device for the app service.</td> </tr>
<tr> <td> </td> <td>SendMessageAsync(ValueSet)</td> <td>傳送 ValueSet 内容到 App Service</td> </tr>
<tr> <td>Events</td> <td>RequestReceived</td> <td>Occurs when a message arrives from the other endpoint of the app service connection.</td> </tr>
<tr> <td> </td> <td>ServiceClosed</td> <td>Occurs when the other endpoint closes the connection to the app service.</td> </tr>
</tbody> </table></div></li>
<li><b><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.Foundation.Collections.ValueSet" target="_blank">ValueSet Class</a></b><br />
<div>實現來傳遞交換訊息用的結構,利用 <span class="inline-code">string</span> 做為 Key,Value 則是 <span class="inline-code">Object</span>。這個結構不能放不可被序列化的結構。</div></li>
<li><b><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.ApplicationModel.AppService.AppServiceResponseStatus
" target="blank">AppServiceResponseStatus</a></b><br />
<div>代表 App 傳送訊息到 App Service 的結果</div></li>
<li><b><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.ApplicationModel.AppService.AppServiceClosedStatus" target="blank">AppServiceClosedStatus</a></b><br />
<div>代表 App Service 被關閉連線時的描述</div></li>
</ul>======<br />
從上面的介紹,如果您的 App Service 是比較工具型跟 App 本身不需要共用資料或邏輯,建議獨立成 Background Task;相反地,需要讓 App Service 跟 App 有互動,建議改為 Same Process 的架構。更多介紹可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/extend-your-app-with-services-extensions-packages" target="_blank">Extend your app with services, extensions, and packages</a>。<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/connected-apps-and-devices" target="_blank">Connected apps and devices (Project Rome)</a> 支援更豐富的功能,下一篇將有更詳細的説明。<br />
希望對大家有所幫助。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-and-consume-an-app-service" target="_blank">Create and consume an app service</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/convert-app-service-in-process" target="_blank">Convert an app service to run in the same process as its host app</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/extend-your-app-with-services-extensions-packages" target="_blank">Extend your app with app services, extensions, and packages</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/how-to-create-an-extension" target="_blank">Create and consume an app extension</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/communicate-with-a-remote-app-service" target="_blank">Communicate with a remote app service</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/AppServices" target="_blank">Universal Windows Platform (UWP) app samples for App Service</a></li>
<li><a href="https://blogs.windows.com/buildingapps/2017/01/25/calling-windows-10-apis-desktop-application/#IrPFReOrwrG0VyGc.97" target="_blank">Calling Windows 10 APIs From a Desktop Application</a></li>
<li><a href="https://github.com/qmatteoq/DesktopBridge" target="_blank">qmatteoq/DesktopBridge</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/support-your-app-with-background-tasks" target="_blank">Support your app with background tasks</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/app-lifecycle" target="_blank">Windows 10 universal Windows platform (UWP) app lifecycle</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/communicate-with-a-remote-app-service" target="_blank">Communicate with a remote app service</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/connected-apps-and-devices" target="_blank">Connected apps and devices (Project Rome)</a></li>
<li><a href="http://no2don.blogspot.com/2015/09/uwp-windows-10-iot-app-service.html" target="_blank">[UWP] Windows 10 IoT App Service 簡單實作 </a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/launch-resume/debug-a-background-task" target="_blank">偵錯背景工作</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-56578699572691209102018-06-11T09:07:00.002+08:002018-06-11T09:07:55.312+08:00UWP - B2B 確認 In-app 購物記錄上一篇 <a href="https://poumason.blogspot.com/2017/01/uwp-in-app-product-purchases.html" target="_blank">UWP - in-app product purchases</a> 介紹了整合内部購買的機制。本篇介紹怎麽做 B2B 確認用戶購買是否成功。<br />
<a name='more'></a><br />
開始介紹 B2B 的開發步驟前,請先閲讀兩個重要的文件:<br />
<ol><li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/Store" target="_blank">Windows-universal-samples/Samples/Store/</a>:重要的範例</li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/publish/add-on-submissions" target="_blank">附加元件提交</a>:幫助瞭解 Add-ons 的類型與流程<div><ul><li>Windows 10 (1607 以上)支援新的類型:<a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/enable-subscription-add-ons-for-your-app" target="_blank">Subscription</a> (訂閲)。搭配 <b>Windows.Services.Store</b> 使用。</li>
<li>Subscription 可以設定每次續約的周期/價格,是否有試用周期,開放購買的對象(beta 測試會需要)。</li>
<li><b>需注意價格預設是免費,發佈之後就不能再往上調整,只能往下調整,所以第一次設定時要考慮好定價。</b></li>
<li>用戶可以隨時從 <a href="https://account.microsoft.com/account" target="_blank">Microsoft Account</a> 中取消訂閲。</li>
<li>可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials#testing" target="_blank">Test your in-app purchase or trial implementation</a> 進行測試。</li>
</ul></div></li>
</ol>MS IAP APIs 提供 RSET 幫助 B2B 管理用戶的 in-app 產品:<br />
<ol><li><i>Microsoft Store collection API</i>:查詢用戶已經購買的產品與回報消費性產品已經消費完畢。</li>
<li><i>Microsoft Store purchase API</i>:指定用戶享受免費產品,抓到用戶已經訂閲的產品與改變訂閲用戶的付款狀態</li>
</ol>詳細可參考:<a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service" target="_blank">Manage product entitlements from a service</a>。<br />
<br />
如何使用 Collection API 與 Purchase API,以及 Store 與自己開發的 Service 是怎麽運作的呢?<br />
利用下圖描述:<br />
<a href="https://2.bp.blogspot.com/-GKKg5rD_F2g/WxzFVfCkEAI/AAAAAAAAA1c/IA5zLQnpZr4Nsuc5TI9zfeT2rNHRWpdtACLcBGAs/s1600/in_app_purchase.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" height="313" src="https://2.bp.blogspot.com/-GKKg5rD_F2g/WxzFVfCkEAI/AAAAAAAAA1c/IA5zLQnpZr4Nsuc5TI9zfeT2rNHRWpdtACLcBGAs/s640/in_app_purchase.jpg" width="640" /></a><br />
<br />
<br />
上圖的分成兩大部分:<br />
<ul><li>For Service:<div><ol><li>先在把 my billing server domain 註冊到 Azure AD 變成 Azure AD Web application,來拿到 tenant_id 與 client_id (或稱 Application ID),細部設定參考: <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service#step-1-configure-an-application-in-azure-ad" target="_blank">Configure an application in Azure AD</a></li>
<li>把 client_id 加入到 Windows Dev Center 中該 App 的<a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service#step-2-associate-your-azure-ad-application-id-with-your-app-in-windows-dev-center" target="_blank">訂閲服務關聯設定</a></li>
<li>在 my billing server 加入負責建立 AD Access Token 與綁定來自 App 的 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service#claims-in-a-microsoft-store-id-key" target="_blank">MS Store ID</a> 的邏輯,關於建立 AD Access token 的邏輯,可以參考<a href="#generateAccessToken">下方的説明</a>。</li>
<li>負責保存與更新 MS Store ID (MS Store ID 有 90 天使用期限,需利用 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/renew-a-windows-store-id-key" target="_blank">Renew MS Store ID Key</a> 在 Server 幫用戶更新)</li>
<li>操作 Collection APIs 檢查用戶購買的產品或是 Purchase APIs 抓取用戶的訂閲期限與更新帳單資訊</li>
</ol></div></li>
<li>For Client:<div><ol><li>由於建立 MS Store ID 需要 AD Access Token (collection/purchase),所以需要先 my billing server 請求資料</li>
<li>搭配 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.services.store.storecontext.getcustomercollectionsidasync" target="_blank">StoreContext.GetCustomerCollectionsIdAsync</a> 與 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.services.store.storecontext.getcustomerpurchaseidasync" target="_blank">StoreContext.GetCustomerPurchaseIdAsync</a> 將拿到的兩種 AD Access Token 分別建立對應的 MS Store ID,並回報給 my billing server 保存</li>
<li>利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.services.store.storecontext" target="_blank">StoreContet</a> 購買 Add-ons 產品 (需注意如果設備沒有登入 MS Account 是無法購買的)</li>
</ol></div></li>
</ul><br />
要完成 Client/Server 整合才能操作 purchase/collection APIs,如下步驟:<br />
<b id="generateAccessToken">Step1: 抓取用到的 AD Access token,主要分成 3 種</b>:<br />
<ul><li><span class="inline-code">https://onestore.microsoft.com</span> : 一定要建立的,因爲它被用在向 REST APIs 發出請求時,放在呼叫 Collection APIs 或 Purchase APIs 的 <b>HTTP Header authorization</b> 驗證值;</li>
<li><span class="inline-code">https://onestore.microsoft.com/b2b/keys/create/collections</span>:要存取 collections 系列的 APIs,需要建立它的 Access token,並傳給 Client 建立 MS Store ID,才能代表 User 操作 Collection APIs;</li>
<li><span class="inline-code">https://onestore.microsoft.com/b2b/keys/create/purchase</span>:要存取 purchase 系列的 APIs,需要建立它的 Access token,並傳給 Client 建立 MS Store ID,才能代表 User 操作 Purchase APIs;</li>
</ul>詳細可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service#step-5-call-the-microsoft-store-collection-api-or-purchase-api-from-your-service" target="_blank">Step 5: Call the Microsoft Store collection API or purchase API from your service</a>。<br />
<br />
如下程式,分別建立 3 種 Access Token,並回傳給 Client:<br />
<pre class="code prettyprint"><code class="language-csharp">[HttpGet]
public async Task<authresultdata> Get()
{
// 1. get header AAD access token
AuthResultData result = new AuthResultData();
result.Auth = await GetAzureADAccesToken(AuthType.Auth);
result.Collection = await GetAzureADAccesToken(AuthType.Collection);
result.Purchase = await GetAzureADAccesToken(AuthType.Purchase);
return result;
}
private async Task<string> GetAzureADAccesToken(AuthType type)
{
string tenantId, clientId, clientSecret;
string resource = string.Empty;
switch (type)
{
case AuthType.Collection:
resource = "https://onestore.microsoft.com/b2b/keys/create/collections";
break;
case AuthType.Purchase:
resource = "https://onestore.microsoft.com/b2b/keys/create/purchase";
break;
default:
resource = "https://onestore.microsoft.com";
break;
}
FormUrlEncodedContent postContent = new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", clientId },
{ "client_secret", clientSecret },
{ "resource", resource },
});
postContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
using (HttpClient client = new HttpClient())
{
var response = await client.PostAsync($"https://login.microsoftonline.com/{tenantId}/oauth2/token", postContent);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return string.Empty;
}
var responseContent = await response.Content.ReadAsStringAsync();
var jsonObject = JsonConvert.DeserializeObject<azureadresponsedata>(responseContent);
return jsonObject.access_token;
}
}</code></pre><br />
<b>Step2: Client App 向 my billing server 拿到 collection/purchase APIs 的 Access Token,產生對應的 MS Store IDs 並回傳給 server</b>:<br />
<pre class="code prettyprint"><code class="language-csharp">private async Task GenerateMicrosoftStoreID()
{
// Request my billing server to get collection/purchase API access token
var authResult = await GetTokenFromAzureOAuthAsync();
// publisherUserId is identify user on your server, such as: serial id, not Microsoft Account
string publisherUserId = "poumason@live.com";
// Generate collection / purchase Id must using difference access token
var collectionStoreId = await storeContext.GetCustomerCollectionsIdAsync(authResult.Collection, uid);
var purchaseStoreId = await storeContext.GetCustomerPurchaseIdAsync(authResult.Purchase, uid);
// Report to my billing server to keep MS Store ID
var actionData = new PostActionData()
{
UID = uid,
AuthData = authResult,
CollectionStoreID = collectionStoreId,
PurchaseStoreID = purchaseStoreId
};
HttpClient client = new HttpClient();
var content = new HttpStringContent(actionData.Stringify());
content.Headers.ContentType = new HttpMediaTypeHeaderValue("application/json");
var result = await client.PostAsync(new Uri("http://mybillingserver.azurewebsites.net/api/inapps"), content);
var responseContent = await result.Content.ReadAsStringAsync();
}</code></pre><br />
<b>Step3: my billing server 呼叫 collection / purchase APIs 獲取用戶已經購買的或訂閲的商品</b>:<br />
取得用戶已經訂閲的商品,API 用法可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/get-subscriptions-for-a-user" target="_blank">Get subscriptions for a user</a>:<br />
<pre class="code prettyprint"><code class="language-csharp">private async Task<string> GetSubscription(string accessToken, string storeID)
{
var purchase = new PurchaseQueryData
{
B2BKey = storeID
};
StringContent postContent = new StringContent(JsonConvert.SerializeObject(purchase));
postContent.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Host = "purchase.mp.microsoft.com";
var response = await client.PostAsync("https://purchase.mp.microsoft.com/v8.0/b2b/recurrences/query", postContent);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return string.Empty;
}
var responseContent = await response.Content.ReadAsStringAsync();
return responseContent;
}
}</code></pre>取得用戶已經購買的商品,API 用法可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/query-for-products" target="_blank">Query for products</a>:<br />
<pre class="code prettyprint"><code class="language-csharp">private async Task<string> QueryOfProduct(string accessToken, string storeID, string uid)
{
var collection = new CollectionData();
collection.Beneficiaries.Add(new UserIdentityData
{
Reference = uid,
Value = storeID
});
collection.ProductTypes.Add("Application");
collection.ProductTypes.Add("Durable");
collection.ProductTypes.Add("UnmanagedConsumable");
StringContent content = new StringContent(JsonConvert.SerializeObject(collection));
content.Headers.ContentType = new MediaTypeHeaderValue("application/json");
using (HttpClient client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
client.DefaultRequestHeaders.Host = "collections.mp.microsoft.com";
var response = await client.PostAsync(new Uri(https://collections.mp.microsoft.com/v6.0/collections/query), content);
if (response.StatusCode != System.Net.HttpStatusCode.OK)
{
return string.Empty;
}
var responseContent = await response.Content.ReadAsStringAsync();
return responseContent;
}
}</code></pre>再强調一次,從 server 要呼叫 collection / purchase APIs 時, HTTP Request Header 中 Authorization 需給與來自 <span class="inline-code">https://onestore.microsoft.com</span> 的 Access Token,而 APIs 内給的 body 就分別給與 MS Store ID 來代表用戶。<br />
<br />
分別在 Client App 與 Server 完成上面的事情,既可以讓 Server 拿到 MS Store ID (代表用戶) 來查詢該用戶購買的資訊,完成一些檢驗的流程,例如:購買確認,發票開立等。<br />
更多其他的說明,可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service" target="_blank">Manage product entitlements from a service</a>。<br />
<br />
[<b>補充</b>]<br />
<ul><li><a href="https://docs.microsoft.com/zh-tw/azure/active-directory/develop/active-directory-token-and-claims" target="_blank">Claims in a Microsoft Store ID key</a><div>利用 JSON Web Token (JWT) 格式建立,裏面包含許多資料,其中 <span class="inline-code">http://schemas.microsoft.com/marketplace/2015/08/claims/key/userId</span> 最重要,因爲它不是 Microsoft Account,它代表這個用戶在你的 server 中的識別值,它來自于 StoreContext.GetCustomerCollectionsIdAsync 或 StoreContext.GetCustomerPurchaseIdAsync 方法中所給予的 publisherUserId。</div></li>
</ul>======<br />
B2B 整合 MS IAP APIs 其實不困難,最麻煩是 <u>時間差</u>。<br />
在 <a href="https://dev.windows.com/zh-tw/" target="_blank">Windows Dev Center</a> 加入 add-on 產品與收到 email 通知上架後,需再等大約 16 小時之後,才能在 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.services.store.storecontext" target="_blank">StoreContext</a> 看到資料。同樣地,整合 APIs 在註冊與 App 關聯之後也要等超過 16 小時才能拿到資料。<br />
所以建立在開發前,建議先把要販售的商品/訂閲,Azure AD 註冊 web application,以及關聯 Windows Dev Center 都設定好,過 1 天之後再開發會比較完整。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/publish/set-your-add-on-product-id" target="_blank">Set your add-on product type and product ID</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials#testing" target="_blank">Test your in-app purchase or trial implementation</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/Store" target="_blank">Windows-universal-samples/Samples/Store/</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/monetize/in-app-purchases-and-trials" target="_blank">App 內購買和試用版</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/publish/add-on-submissions" target="_blank">附加元件提交</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/monetize/manage-add-on-submissions" target="_blank">管理附加元件提交</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/view-and-grant-products-from-a-service" target="_blank">Manage product entitlements from a service</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/get-subscriptions-for-a-user" target="_blank">Get subscriptions for a user</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/query-for-products" target="_blank">Query for products</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/publish/choose-visibility-options#audience" target="_blank">選擇可見度選項</a> (設定 Beta 測試或是商品可以透過 Store 或只在 App 内購買)</li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/renew-a-windows-store-id-key" target="_blank">Renew a Microsoft Store ID key</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/monetize/in-app-purchases-and-trials" target="_blank">In-app purchases and trials</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com2tag:blogger.com,1999:blog-2649688415868412622.post-45147292147226166952018-05-11T14:14:00.000+08:002018-05-11T14:14:32.099+08:00UWP - 同一個 App 顯示多個視窗爲了讓用戶在 Desktop 上操作 UWP app 有更好的生產力,可以利用 Multi-Windows 的技術,讓 App 操作上更接近 Win32 程式的體驗。本篇介紹怎麽使用。<br />
<a name='more'></a><br />
Multiple View/Windows 最典型的例子就是官方文件的這張圖:<br />
<img alt="Wireframe showing an app with multiple windows" class="x-hidden-focus" data-linktype="relative-path" src="https://docs.microsoft.com/en-us/windows/uwp/design/layout/images/multi-view.png" style="border: 0px none rgb(0, 0, 0); display: inline-block; font-family: segoe-ui_normal, "Segoe UI", Segoe, "Segoe WP", "Helvetica Neue", Helvetica, sans-serif; font-size: 16px; font-variant-east-asian: normal; font-variant-numeric: normal; height: 356.4px; max-width: 712.8px;" /><br />
幾個瞭解使用 Multiple views/windows 前要注意:<br />
<ol><li>UWP 處理 Multiple views/windows 時與 WPF/Win32 程式不一樣的地方:<b><span class="inline-code">所有 application views 使用各自的 threads</span></b></li>
<li>每一個 Windows 有自己的 task bar,用戶可以在同時操作多個 windows</li>
<li>App 支援把原本的 View 獨立成一個 Window, 也要支援可以合併回到 Main App 之中</li>
</ol>什麽狀況適合使用 multiple views?<br />
<ol><li>email app, 讓用戶可以同時讀取多封内容,或是獨立 window 撰寫不需要打斷邊讀邊寫的狀況</li>
<li>contact app, 讓用戶可以開多個 contact info 做比對</li>
<li>music player app, 讓邊聽歌也可以邊看其他可以播放的音樂資訊</li>
<li>note-taking app 讓用戶可以複製内容做備注是用</li>
<li>閲讀 App 讓用戶可以邊閲讀,邊打開作者其他作品或是相關内容,做筆記等</li>
</ol>另外,在 Windows 10 (1803) 開始支援 <a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp" target="_blank">multiple-instance</a>,讓 App 可以一次開多個,這樣能做的事情更多了。<br />
<br />
接著根據官方説明,定義一下 View 的概念:<br />
<ol><li><b>app view 代表一個 thread 對應一個 window</b>,app 使用它來顯示。代表的是一個 <a href="https://msdn.microsoft.com/library/windows/apps/br225017" target="_blank">Windows.ApplicationModel.Core.CoreApplicationView</a></li>
<li>View 被 <a href="https://msdn.microsoft.com/library/windows/apps/br225016" target="_blank">CoreApplication</a> 管理,可利用 <a href="https://msdn.microsoft.com/library/windows/apps/dn297278" target="_blank">CoreApplication.CreateNewView</a> 建立新的 <a href="https://msdn.microsoft.com/library/windows/apps/br225017" target="_blank">CoreApplicationView</a></li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/br225017" target="_blank">CoreApplicationView</a> 由 <a href="https://msdn.microsoft.com/library/windows/apps/br225019" target="_blank">CoreWindow</a> 與 <a href="https://msdn.microsoft.com/library/windows/apps/dn433264" target="_blank">CoreDispatcher</a> 組成,可以識別為 Windows Runtime 用來與 Windows System 互動的元件</li>
<li>通常不直接使用 <a href="https://msdn.microsoft.com/library/windows/apps/br225017" target="_blank">CoreApplicationView</a>,在 Windows Runtime 提供 <a href="https://msdn.microsoft.com/library/windows/apps/br242295" target="_blank">Windows.UI.ViewManagement</a> 裡的 <a href="https://msdn.microsoft.com/library/windows/apps/hh701658" target="_blank">ApplicationView</a> 來操作</li>
<li><a href="https://msdn.microsoft.com/library/windows/apps/hh701658" target="_blank">ApplicationView</a> 提供許多屬性,方法與事件,讓我們方便操作 windowing system</li>
<li>利用 <a href="https://msdn.microsoft.com/library/windows/apps/hh701672" target="_blank">ApplicationView.GetForCurrentWindow</a> 拿到 <a href="https://msdn.microsoft.com/library/windows/apps/hh701658" target="_blank">ApplicationView</a> 實體,而它與 <a href="https://msdn.microsoft.com/library/windows/apps/br225017" target="_blank">CoreApplicationView</a> 的 thread 是綁在一起的</li>
<li>XAML framework 包裝 <a href="https://msdn.microsoft.com/library/windows/apps/br208225" target="_blank">CoreWindow</a> 物件在 <a href="https://msdn.microsoft.com/library/windows/apps/br209041" target="_blank">Windows.UI.XAML.Window</a> 物件,而在 XAML 操作用 Window 物件操作 <a href="https://msdn.microsoft.com/library/windows/apps/br208225" target="_blank">CoreWindow</a></li>
</ol><br />
<b>顯示一個新的 View</b><br />
建議在在明顯的位置放入一個 <span class="inline-code">new window</span> 的按鈕,幫助使用者知道這個 view 可以分割到另一個新 window。<br />
或者是在 context menu 加入 <span class="inline-code">Open in a new window</span>。<br />
透過下面的範例程式簡單説明,如何建立一個新的 view:<br />
<pre class="code prettyprint"><code class="language-csharp">private async void Button_Click(object sender, RoutedEventArgs e)
{
CoreApplicationView newView = CoreApplication.CreateNewView();
int newViewId = 0;
await newView.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
Frame frame = new Frame();
frame.Navigate(typeof(SecondaryPage), null);
Window.Current.Content = frame;
// You have to activate the window in order to show it later.
Window.Current.Activate();
newViewId = ApplicationView.GetForCurrentView().Id;
});
bool viewShown = await ApplicationViewSwitcher.TryShowAsStandaloneAsync(newViewId);
}</code></pre><br />
上述程式碼中幾個重點:<br />
<ul><li><a href="https://msdn.microsoft.com/library/windows/apps/hh701672" target="_blank">ApplicationView.GetForCurrentView</a> 可以抓出該 thread 中 view 的 Id,搭配 <a href="https://msdn.microsoft.com/library/windows/apps/dn281097" target="_blank">ApplicationViewSwitcher</a> 要切換到哪一個 view 上。可以參考 <a href="http://go.microsoft.com/fwlink/p/?LinkId=620574" target="_blank">MultipleViews Sample</a> 建立的 <b>ViewLifetimeControl</b> 可以管理被建立出來的 views (非常建議使用)。<div>補充 ViewLifetimeControl 的重點:<br />
<ol><li>new view 的 page 記得註冊 ViewLifetimeControl 的 Released 事件,幫助關閉 secondary view 時做一些處理,範例:<div><pre class="code prettyprint"><code class="language-csharp">private async void ViewLifetimeControl_Released(Object sender, EventArgs e)
{
((ViewLifetimeControl)sender).Released -= ViewLifetimeControl_Released;
// The ViewLifetimeControl object is bound to UI elements on the main thread
// So, the object must be removed from that thread
await mainDispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
((App)App.Current).SecondaryViews.Remove(thisViewControl);
});
// The released event is fired on the thread of the window
// it pertains to.
//
// It's important to make sure no work is scheduled on this thread
// after it starts to close (no data binding changes, no changes to
// XAML, creating new objects in destructors, etc.) since
// that will throw exceptions
Window.Current.Close();
}</code></pre></div></li>
</ol></div></li>
<li>CoreDispatcher.RunAsync 建立工作排程,用 lambda expression 寫的内容會在 new view 所屬的 thread 中被執行。</li>
<li>在 new View 中設定好 Window 之後,要記得使用 <span class="inline-code">Window.Current.Activate();</span> 該 View 才會被啓動</li>
<li>可利用 <a href="https://msdn.microsoft.com/library/windows/apps/dn281101" target="_blank">ApplicationViewSwitcher.TryShowAsStandaloneAsync</a> 要求顯示特定的 View。另外可以使用 <a href="https://msdn.microsoft.com/library/windows/apps/dn281109" target="_blank">ApplicationView.GetApplicationViewIdForWindow</a> 來抓取目前 Window 的 View Id</li>
</ul><br />
上述的程式範例中, Main View 與 Secondary View 的定義其實有些模糊,根據<a href="https://docs.microsoft.com/en-us/windows/uwp/design/layout/show-multiple-views" target="_blank">官方文件</a>的介紹,透過下面説明:<br />
<br />
<b>Main View</b><br />
<ul><li>當 App 被啓動時預設會建立一個 main view (第一個 view),它被保存在 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.mainview#Windows_ApplicationModel_Core_CoreApplication_MainView" targt="_blank">CoreApplication.MainView</a> 裏面,它的 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplicationview.ismain#Windows_ApplicationModel_Core_CoreApplicationView_IsMain" target="_blank">IsMain = true</a>。</li>
<li>main view 的 thread 負責該 App 上面的所有事件與畫面控制。</li>
<li>如果 secondary view 被開啓,main view 的 window 會被隱藏,例如:按下 close (X) 在 window title bar ,但它的 thread 還活著,在 main view 的 window 呼叫 close() 會得到 InvaildOperationException。</li>
<li>可利用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.application.exit" target="_blank">Application.Exit</a> 關閉 App。如果 main view 的 thread 被結束,代表 app 被關閉。</li>
</ul><br />
<b>Secondary views</b><br />
<ol><li>透過 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.createnewview#Windows_ApplicationModel_Core_CoreApplication_CreateNewView" target="_blank">CoreApplication.CreateNewView()</a> 建立的 view 都算是 secondary views。main view 與 secondary views 被保存在 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.views#Windows_ApplicationModel_Core_CoreApplication_Views" target="_blank">CoreApplication.Views</a> 集合裡。</li>
<li>通常會建立 secondary views 都來自使用者自行點擊,部分會來自系統的需求(例如:使用 <a href="https://technet.microsoft.com/library/mt219050.aspx" target="_blank">kiosk mode</a>,系統會自動建立 secondary view 在 lock screen 上顯示。在使用 Kiosk mode 時不支援自己建立 secondary view,如果建立了會造成 exception)<br />
</li>
</ol><br />
最後介紹幾個重要的元素:<br />
<ul><li><b><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationviewswitcher" target="_blank">ApplicationViewSwitcher Class</a></b><div>負責 app view 交換的行爲。常用的 methods:<br />
<table border="1"><tbody>
<tr><td>Method Name</td><td>Description</td></tr>
<tr><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationviewswitcher.tryshowasstandaloneasync#Windows_UI_ViewManagement_ApplicationViewSwitcher_TryShowAsStandaloneAsync_System_Int32_" target="_blank">TryShowAsStandaloneAsync</a></td><td>在螢幕上為 App 顯示與原始視窗相鄰的另一個視窗。<br />
該 method 只能在 ASTA(core UI) thread 中使用。<br />
每一個新建立的 view 都有自己的 UI thread(ASTA) 與相關的 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.core.corewindow" target="_blank">CoreWindow</a>。<br />
要注意使用 thread-safe (例如:<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.core.coredispatcher" target="_blank">CoreDispatcher</a>) 讓 window 之間可以互動溝通。</td></tr>
<tr><td>TryShowAsStandaloneAsync(Int32, ViewSizePreference)</td><td>在螢幕上為 App 顯示指定另一個特定 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.viewsizepreference" target="_blank">ViewSizePreference</a> 的視窗。</td></tr>
</tbody></table></div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.viewsizepreference" target="_blank">ViewSizePreference</a><div>定義 window 可能的顯示 size。<br />
<table border="1"><tbody>
<tr><td>Custom</td><td>6</td><td>window 使用自定義 size 來顯示</td></tr>
<tr><td>Default</td><td>0</td><td>window 不指定 size,改用預設(UseHalf)</td></tr>
<tr><td>IseHalf</td><td>2</td><td>window 使用 50% 的可視水平畫面為 size</td></tr>
<tr><td>UseLess/td></td><td>1</td><td>window 使用低於 50% 的可視水平畫面為 size</td></tr>
<tr><td>UseMinimum</td><td>4</td><td>window 使用最小可視水平畫面(320 或 500 pixels)為 size</td></tr>
<tr><td>UseMore</td><td>3</td><td>window 使用高於 50% 的可視水平畫面為 size</td></tr>
<tr><td>UseNone</td><td>5</td><td>window 沒有可見元件</td></tr>
</tbody></table></div></li>
<li><b><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication" target="_blank">CoreApplication</a></b><div>使應用程式能夠處理狀態更改、管理 windows 以及與各種 UI 框架組成。<br />
系統在運行應用程式時將此物件作為單一實例創建。它被當作 Application Single Threaded Apartment (ASTA) 來運行。而從 Singleton 建立的 threads 應歸因於多執行緒單元 (MTAThread)。<br />
<table border="1"><tbody>
<tr><td>Type</td><td>Name</td><td>Description</td></tr>
<tr><td>Properties</td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.mainview#Windows_ApplicationModel_Core_CoreApplication_MainView">MainView</a></td><td>取得使用此 CoreApplication 實例化的所有正在運行的 CoreApplicationView</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.views#Windows_ApplicationModel_Core_CoreApplication_Views">Views</a></td><td>取得 app 所有的 views</td></tr>
<tr><td>Methods</td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.createnewview#Windows_ApplicationModel_Core_CoreApplication_CreateNewView">CreateNewView()</a></td><td>為 App建立新的 view</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.exit#Windows_ApplicationModel_Core_CoreApplication_Exit" target="_blank">Exit()</a></td><td>關閉 App</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.getcurrentview#Windows_ApplicationModel_Core_CoreApplication_GetCurrentView">GetCurrentView()</a></td><td>設定 view 被啓用</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.applicationmodel.core.coreapplication.requestrestartasync#Windows_ApplicationModel_Core_CoreApplication_RequestRestartAsync_System_String_">RequestRestartAsync(String)</a></td><td>重新啓動 App</td></tr>
</tbody></table></div></li>
<li><b><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview" target="_blank">ApplicationView</a></b><div>代表活動中的 application view 與相關的狀態/行爲。<br />
window(或稱 app view) 是 Windows Runtime app 的顯示部分。使用者螢幕可以同時顯示多達4個可變寬度視窗。它們不重疊, 其頂部和底部邊緣觸及螢幕的頂部和底部邊緣。相鄰視窗之間可能存在非視窗區域。<br />
window 與 page 不一樣,它比較像是 pages 的容器。可以在程式中對應用程式的所有頁使用視窗引用。<br />
每一個 window 對應一個 CoreWindow,它代表 UI process thread (core input handlers 與 event dispatcher)。<br />
<table border="1"><tbody>
<tr><td>Type</td><td>Name</td><td>Description</td></tr>
<tr><td>Properties</td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.adjacenttoleftdisplayedge#Windows_UI_ViewManagement_ApplicationView_AdjacentToLeftDisplayEdge">AdjacentToLeftDisplayEdge</a></td><td>告訴您螢幕的左邊緣是否為視窗的左邊框</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.isfullscreenmode#Windows_UI_ViewManagement_ApplicationView_IsFullScreenMode">IsFullScreenMode</a></td><td>取得/設定 App 是否為 full-screen 模式</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.preferredlaunchviewsize#Windows_UI_ViewManagement_ApplicationView_PreferredLaunchViewSize">PreferredLaunchViewSize</a></td><td>設定或取得當 app 被啓動時預期的 size,<br />
需要搭配 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.preferredlaunchwindowingmode#Windows_UI_ViewManagement_ApplicationView_PreferredLaunchWindowingMode">PreferredLaunchWindowingMode</a> 屬性一起使用(設定為 PreferredLaunchViewSize)</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.preferredlaunchwindowingmode#Windows_UI_ViewManagement_ApplicationView_PreferredLaunchWindowingMode">PreferredLaunchWindowingMode</a></td><td>設定或取得 app 啓動時視窗模式的值,<br />
搭配 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.preferredlaunchviewsize#Windows_UI_ViewManagement_ApplicationView_PreferredLaunchViewSize">PreferredLaunchViewSize</a> 一起使用。<br />
可以設定的值有:<br />
<ul><li>Auto:系統會自動調整應用程式視窗的大小。</li>
<li>FullScreen:視窗是全屏幕。</li>
<li>PreferredLaunchViewSize:視窗的大小由 ApplicationView. PreferredLaunchViewSize 屬性指定。</li>
</ul></td></tr>
<tr><td>Methods</td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.getapplicationviewidforwindow#Windows_UI_ViewManagement_ApplicationView_GetApplicationViewIdForWindow_Windows_UI_Core_ICoreWindow_">GetApplicationViewIdForWindow(ICoreWindow)</a></td><td>利用 CoreWindow 取得他的 window ID</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.getforcurrentview#Windows_UI_ViewManagement_ApplicationView_GetForCurrentView">GetForCurrentView()</a></td><td>取得啓動程式的 view state 與 behavior settings</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.tryconsolidateasync#Windows_UI_ViewManagement_ApplicationView_TryConsolidateAsync">TryConsolidateAsync()</a></td><td>嘗試關閉現在的 view。這個 method 等同於用戶在 app view 點了 close。</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.tryresizeview#Windows_UI_ViewManagement_ApplicationView_TryResizeView_Windows_Foundation_Size_">TryResizeView(Size)</a></td><td>嘗試調整顯示的 size。</td></tr>
<tr><td>Events</td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.consolidated">Consolidated</a></td><td>發生在 window 被從最近使用的程式清單中移除,或是用戶執行關閉筆勢時發生。</td></tr>
<tr><td></td><td><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.visibleboundschanged">VisibleBoundsChanged</a></td><td>當 VisibleBounds 的值發生更改時, 將引發此事件, <br />
通常是顯示或隱藏的狀態列、應用程式欄或其他 chrome 的結果。</td></tr>
</tbody></table></div></li>
</ul><br />
[<b>補充</b>]<br />
<ul><li>如果希望記錄每一個 secondary views 最後的 size,讓下一次開啓的時候可以恢復原本的 window size,要怎麽做呢?<div>關鍵元素:<b><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.viewmanagement.applicationview.preferredlaunchwindowingmode#Windows_UI_ViewManagement_ApplicationView_PreferredLaunchWindowingMode" target="_blank">ApplicationView.PreferredLaunchWindowingMode</a></b>。<br />
<ol><li>搭配 <a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/MultipleViews" target="_blank">Multiple views sample</a> 的 SecondaryViewPage 爲例,加入以下的 Code:<div><pre class="code prettyprint"><code class="language-csharp">// 記錄 SecondaryViewPage 所屬的 View Id
int secondaryViewId;
protected override void OnNavigatedTo(NavigationEventArgs e)
{
thisViewControl = (ViewLifetimeControl)e.Parameter;
mainViewId = ((App)App.Current).MainViewId;
mainDispatcher = ((App)App.Current).MainDispatcher;
// When this view is finally release, clean up state
thisViewControl.Released += ViewLifetimeControl_Released;
// 檢查是否為非 MainViewId, 避免設定錯誤
secondaryViewId = ApplicationView.GetForCurrentView().Id;
if (mainViewId != secondaryViewId)
{
// 重點: 把 PreferredLaunchWindowingMode 設定為 PreferredLaunchViewSize
// 讓 SecondaryViewPage 在出現時變成自定義視窗大小, TryResizeView 才會成功
ApplicationView.PreferredLaunchWindowingMode = ApplicationViewWindowingMode.PreferredLaunchViewSize;
// 註冊事件處理第一次出現該 View 時要調整視窗大小
Window.Current.VisibilityChanged += Window_VisibilityChanged;
}
}</code></pre></div></li>
<li>建立一個 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/app-settings/store-and-retrieve-app-data" target="_blank">AppSettings</a> 儲存 SecondaryViewPage 調整的 Size:<pre class="code prettyprint"><code class="language-csharp">public class AppSettings
{
private ApplicationDataContainer localSettings = ApplicationData.Current.LocalSettings;
public void Set<t>(string key, T value)
{
if (localSettings.Values.ContainsKey(key))
{
localSettings.Values[key] = value;
}
else
{
localSettings.Values.Add(key, value);
}
}
public T Get<t>(string key)
{
if (localSettings.Values.ContainsKey(key))
{
return (T)localSettings.Values[key];
}
else
{
return default(T);
}
}
}</code></pre></li>
<li>在 VisibilityChanged 時處理第一次出現視窗時把視窗大小設定為上一次調整後的結果:<pre class="code prettyprint"><code class="language-csharp">// 用來識別是否已經設定過初始的視窗大小
bool isInitialResize = false;
private void Window_VisibilityChanged(object sender, VisibilityChangedEventArgs e)
{
if (e.Visible && isInitialResize == false)
{
// 檢查是否為 SecondaryViewPage 所屬的 View Id
if (secondaryViewId == ApplicationView.GetApplicationViewIdForWindow(Window.Current.CoreWindow))
{
// 從 App Setting 中得到上次調整大小的結果
var defaultSize = settings.Get<size>(SECONDARY_DEFAULT_SIZE);
if (defaultSize == null)
{
defaultSize = new Size(200,100);
}
// 設定 ApplicationView 的大小,要記得設定 ApplicationViewWindowingMode.PreferredLaunchViewSize ,不然都會是 false
bool result = ApplicationView.GetForCurrentView().TryResizeView(defaultSize);
isInitialResize = true;
// 註冊處理 Size Changed 來保存視窗最後的大小
Window.Current.SizeChanged += Current_SizeChanged;
}
}
else
{
Window.Current.SizeChanged -= Current_SizeChanged;
}
}</code></pre></li>
<li>處理 SizeChanged 把調整大小的結果儲存在 AppSettings<pre class="code prettyprint"><code class="language-csharp"> private void Current_SizeChanged(object sender, WindowSizeChangedEventArgs e)
{
// 要注意是 ActivatedInForeground 才需要記錄
if (Window.Current.CoreWindow.ActivationMode != CoreWindowActivationMode.ActivatedInForeground)
{
return;
}
settings.Set(SECONDARY_DEFAULT_SIZE, e.Size);
}</code></pre></li>
</ol></div></li>
<li>可在畫面中加入一個 <b>open new window</b> 的 glyph,乾净且清楚告訴用戶怎麽使用。例如:<div><pre class="code prettyprint"><code class="language-xml">// C# code
Button btnIcon = new Button();
btnIcon.FontFamily = new FontFamily("Segoe MDL2 Assets");
btnIcon.Content = "\uE8A7";
// XAML
<Button x:Name="btn" Content="&#xE8A7;" FontFamily="Segoe MDL2 Assets" /></code></pre></div></li>
<li>需要確認 single view 時所有功能都能正常使用,分割 secondary views 只是方便使用,不會佔用所有的功能</li>
<li>不要依賴 secondary view 去提供通知或顯示效果</li>
</ul>======<br />
UWP 發展到現在對於 Desktop 的支援度更完整,讓我們在開發的時候可以降低原本習慣 Win32 操作的門檻。<br />
希望這篇對大家有所幫助,謝謝。<br />
<br />
<b>References</b><br />
<ul><li><a href="https://blogs.msdn.microsoft.com/mvpawardprogram/2017/09/19/multiplewindows-in-uwp-mvvm/" target="_blank">Working With Multiple Windows In UWP Using MVVM</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/launch-resume/multi-instance-uwp" target="_blank">Create a multi-instance UWP app</a></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/layout/show-multiple-views" target="_blank">Show multiple views for an app</a> / <a href="https://msdn.microsoft.com/en-us/library/windows/apps/dn434070.aspx" target="_blank">Guidelines for multiple windows</a></li>
<li><a href="https://www.pedrolamas.com/2018/03/23/building-a-dispatcher-agnostic-view-model/" target="_blank">Building a dispatcher agnostic view-model</a></li>
<li><a href="https://www.pedrolamas.com/2018/04/19/building-a-multi-window-dispatcher-agnostic-view-model/" target="_blank">Building multi-window dispatcher agnostic view-model</a></li>
<li><a href="https://gist.github.com/mikoskinen/935608835f27e6d8ecbe63483709ddf1" target="_blank">Simple Event Aggegator for Multi Window communication in UWP </a></li>
<li><a href="https://github.com/rudyhuyn/xUIBinding" target="_blank">rudyhuyn/xUIBinding</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/layout/screen-sizes-and-breakpoints-for-responsive-design" target="_blank">Screen sizes and breakpoints</a></li>
<li><a href="https://github.com/Microsoft/Windows-universal-samples/tree/master/Samples/MultipleViews" target="_blank">Multiple views sample</a></li>
<li><a href="https://www.blogger.com/UWP%20Multi-View%20Communication" target="_blank">UWP Multi-View Communication</a>/ <a href="https://www.youtube.com/watch?v=P2PijMLk2sk" target="_blank">Multiple views in UWP</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/layout/layouts-with-xaml" target="_blank">Responsive layouts with XAML</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.ApplicationModel.Core.CoreApplication#Windows_ApplicationModel_Core_CoreApplication_CreateNewView_System_String_System_String_" target="_blank">CoreApplication Class</a> / <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.ViewManagement.ApplicationView" target="_blank">ApplicationView Class</a> / <a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.ViewManagement.ApplicationViewSwitcher" target="_blank">ApplicationViewSwitcher Class</a></li>
<li><a href="https://code.msdn.microsoft.com/windowsapps/Multiple-Views-Sample-2582fcf3/" target="_blank">Multiple Views Sample(8.1)</a></li>
<li><a href="https://stackoverflow.com/questions/39185575/how-get-different-size-windows-in-uwp-multiple-views" target="_blank">How get different size windows in UWP Multiple Views?</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-50502176817446792018-04-11T02:06:00.001+08:002018-04-11T02:06:17.430+08:00UWP - 調整 WebView 的 UserAgent想要在 UWP 調整 WebView 的 User-Agent 記得先把 App 的 mini SDK version 升級到在 Windows 10 (15063),來看一下是怎麽做的吧。<br />
<a name='more'></a><br />
我參考 <a href="https://gist.github.com/mattdot/3b53af7756c061e06f60623c766f657a" target="_blank">Set the UserAgent for a UWP WebView</a> 的介紹,得知可以從 Win32 API 的 <span class="inline-code">urlmon.dll</span> 做到這個效果。如下程式碼:<br />
<pre class="prettyprint code"><code class="language-csharp">const int URLMON_OPTION_USERAGENT = 0x10000001;
[DllImport("urlmon.dll", CharSet = CharSet.Ansi)]
private static extern int UrlMkSetSessionOption(int dwOption, string pBuffer, int dwBufferLength, int dwReserved);
[DllImport("urlmon.dll", CharSet = CharSet.Ansi)]
private static extern int UrlMkGetSessionOption(int dwOption, StringBuilder pBuffer, int dwBufferLength, ref int pdwBufferLength, int dwReserved);
</code></pre>因此,我就好奇 <span class="inline-code">urlmon.dll</span> 還可以做到那些用途,是否還有其他 Win32 API 可以使用。<br />
<br />
根據 <a href="https://docs.microsoft.com/en-us/uwp/win32-and-com/win32-apis#apis-from-urlmondll" target="_blank">APIs from urlmon.dll</a> 的説明提到:<br />
<table border="1"><tr><td><b>API</b></td><td><b>Requirements</b></td></tr>
<tr><td><a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms775098.aspx" target="_blank">CreateUri</a></td><td>Introduced into urlmon.dll in 10.0.10240. Moved into ext-ms-win-core-iuri-l1-1-0.dll in 10.0.15063.</td></tr>
<tr><td><a href="https://msdn.microsoft.com/en-us/library/windows/desktop/ms775100.aspx" target="_blank">CreateUriWithFragment</a></td><td>Introduced into urlmon.dll in 10.0.10240. Moved into ext-ms-win-core-iuri-l1-1-0.dll in 10.0.15063.</td></tr>
<tr><td><a href="https://msdn.microsoft.com/en-us/library/ms879521.aspx" target="_blank">UrlMkGetSessionOption</a></td><td>Introduced into urlmon.dll in <b>10.0.15063</b>.</td></tr>
<tr><td><a href="https://msdn.microsoft.com/en-us/library/ms879522.aspx" target="_blank">UrlMkSetSessionOption</a></td><td>Introduced into urlmon.dll in <b>10.0.15063</b>.</td></tr>
</table><br />
需要 15063 以上才有支援,而 <span class="inline-code">UrlMkSetSessionOption</span> 調整 UserAgent 的參數值為 <span class="inline-code">0x10000001</span>,參考如下:<br />
<table border="1"><tr><td>Flag</td><td>Value</td><td>Description</td></tr>
<tr><td>URLMON_OPTION_USERAGENT</td><td>0x10000001</td><td>Sets the user agent string for this process.</td></tr>
<tr><td>URLMON_OPTION_USERAGENT_REFRESH</td><td>0x10000002</td><td>Refreshes the user agent string from the registry for this process.</td></tr>
</table><br />
需注意,利用 UrlMkSetSessionOption 的 (0x10000001) 更新 UserAgent 之後,在 App 執行期間是用的 WebView 都是使用修改後的值。<br />
利用下面的程式片段可設定與取得目前的 UserAgent:<br />
<pre class="prettyprint code"><code class="language-csharp">public static class UserAgent
{
const int URLMON_OPTION_USERAGENT = 0x10000001;
[DllImport("urlmon.dll", CharSet = CharSet.Ansi)]
private static extern int UrlMkSetSessionOption(int dwOption, string pBuffer, int dwBufferLength, int dwReserved);
[DllImport("urlmon.dll", CharSet = CharSet.Ansi)]
private static extern int UrlMkGetSessionOption(int dwOption, StringBuilder pBuffer, int dwBufferLength, ref int pdwBufferLength, int dwReserved);
public static string GetUserAgent()
{
// 設定讀取的長度 255 bytes ,因爲是 0x (16 進位數)
int bufferLength = 255;
StringBuilder buffer = new StringBuilder(bufferLength);
int length = 0;
UrlMkGetSessionOption(URLMON_OPTION_USERAGENT, buffer, bufferLength, ref length, 0);
return buffer.ToString();
}
public static void SetUserAgent(string agent)
{
// S_OK, E_INVALIDARG, E_OUTOFMEMORY, E_UNEXPECTED, and E_FAIL
var hr = UrlMkSetSessionOption(URLMON_OPTION_USERAGENT, agent, agent.Length, 0);
// 轉換得到的值看是否為 Exception
var ex = Marshal.GetExceptionForHR(hr);
if(ex!= null)
{
throw ex;
}
}
public static void AppendUserAgent(string appendContent)
{
SetUserAgent(GetUserAgent() + appendContent);
}
}
</code></pre>另外有看過這篇 <a href="https://www.pedrolamas.com/2017/03/21/setting-a-custom-user-agent-in-the-uwp-webview-control/" target="_blank">Setting a custom User-Agent in the UWP WebView control</a> 的介紹,還可以使用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.webview#Windows_UI_Xaml_Controls_WebView_NavigateWithHttpRequestMessage_Windows_Web_Http_HttpRequestMessage_" target="_blank">WebView.NavigateWithHttpRequestMessage</a> 時,另外建立一個 HttpRequestMessage 調整送出的 UserAgent,但是非常複雜。<br />
======<br />
如果您是使用 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient" target="_blank">HttpClient</a> 要調整 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.web.http.headers.httprequestheadercollection#Windows_Web_Http_Headers_HttpRequestHeaderCollection_UserAgent" target="_blank">UserAgent</a> 是很容易的,但是 <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls.webview" target="_blank">WebView</a> 是 UWP 包裝好的要調整就比較麻煩。<br />
因此整理上面的介紹與使用方式,希望有幫助到大家。<br />
<br />
<b>References</b>:<br />
<ul><li><a href="https://gist.github.com/mattdot/3b53af7756c061e06f60623c766f657a" target="_blank">Set the UserAgent for a UWP WebView</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/win32-and-com/win32-apis" target="_blank">APIs present on all Windows 10 devices</a></li>
<li><a href="https://www.pedrolamas.com/2017/03/21/setting-a-custom-user-agent-in-the-uwp-webview-control/" target="_blank">Setting a custom User-Agent in the UWP WebView control</a></li>
<li><a href="https://msdn.microsoft.com/en-us/library/ms879522.aspx" target="_blank">UrlMkSetSessionOption</a></li>
<li><a href="https://msdn.microsoft.com/en-us/library/ms879521.aspx" target="_blank">UrlMkGetSessionOption</a></li>
<li><a href="https://msdn.microsoft.com/en-us/library/windows/desktop/mt186422(v=vs.85).aspx" target="_blank">Dlls for Universal Windows Platform (UWP) apps</a></li>
<li><a href="https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms775100(v=vs.85)" target="_blank">CreateUriWithFragment function</a></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/api/windows.web.http.httpclient" target="_blank">HttpClient Class</a></li>
<li><a href="https://msdn.microsoft.com/en-us/library/ms851554.aspx" target="_blank">URL Moniker Services Functions</a></li>
<li><a href="http://www.cnblogs.com/findumars/p/4999018.html" target="_blank">Delphi下获取IE的UserAgent的方法</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-35872528025136545062018-02-25T16:22:00.000+08:002018-02-28T14:53:31.539+08:00讓 Cortana Skill 幫你唱歌請 Cortana 唱歌是我夢寐以求的功能,因爲 Amazon Alexa / Google Home 都能支援,但是之前 Cortana Skill 不支援。<br />
微軟最新公佈 <a href="https://docs.microsoft.com/en-us/cortana/skills/audio-streaming" target="_blank">Add audio streaming to your skill</a> 就讓我來介紹要怎麽使用。<br />
<a name='more'></a><br />
如果您也研究過 Amazon Alexa 與 Google Home 會明白預設的 Music service (或稱 provider) 均是直接整合到系統裡,<br />
使用者只需要說:"Alexa, play music by [artist]."。<br />
系統就會找到對應的歌手或是歌曲,再轉給整合的 Music service 拿到播放的 streaming。<br />
本篇介紹是<b>利用 Skill 的機制加入播放功能</b>,<u>跟 Cortana 設備提供的 Music Provider 機制不一樣</u>。<br />
<br />
Skill 支援播放 1 ~ N 首歌曲,在每一首歌曲播放完畢自動抓取下一首歌曲,沒有隨機播放,<br />
完全依賴建立在 <a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.connector.audiocard?view=botconnector-3.12.2.4" target="_blank">AudioCard</a> 給的歌曲順序。<br />
目前支援的格式:<br />
<table border="1"><tbody>
<tr><td><b>Media</b></td><td><b>Extension</b></td></tr>
<tr><td>Audio MP3</td><td>mp3</td></tr>
<tr><td>Audio MP4</td><td>m4a</td></tr>
<tr><td>Audio MP4</td><td>aac</td></tr>
<tr><td>Audio WAV</td><td>wav</td></tr>
</tbody></table>因爲 TuneIn 可以在 Cortana 播放,所以我測試之後發現 HLS 也真的可以支援。<br />
<br />
使用 Audio Card 的幾個重點:<br />
<ol><li>一個 Audio Card attachment 只能放一首歌曲,多首歌曲需要建立多個 Audio Card attachment</li>
<li>每一個 Audio Card 只能設定一組 URL,如果設定多組 URL,Cortana 只會播放第一個 URL</li>
<li>設定在 Audio Card 預期播放的歌曲必須是: <b>internet accessible & HTTPS protocol</b></li>
<li>利用 Audio Card 的 <span class="inline-code">Media</span> property 設定歌曲 URL 資訊; <span class="inline-code">Title</span> property 設定歌曲名稱;Cortana 會忽略其他 property (<strike>非常不知道爲什麽</strike>)</li>
</ol>使用範例如下:<br />
<pre class="code prettyprint"><code class="language-csharp">// Create a reply message
Activity reply = activity.CreateReply();
// Set up attachments on the reply
reply.Attachments = new List<attachment>();
// Add a single media URL for Cortana to play
MediaUrl murl = new MediaUrl("https://{yourstreamurl}");
MediaUrl[] medias = new MediaUrl[] {murl};
// Create a new AudioCard and attach your media URL
AudioCard audioCard = new AudioCard()
{
Media = medias,
};
// Add the attachment and send the reply
Attachment audioCardAttach = audioCard.ToAttachment();
reply.Attachments.Add(audioCardAttach);
reply.InputHint = InputHints.AcceptingInput;
await connector.Conversations.ReplyToActivityAsync(reply);</attachment></code></pre>需注意 <span class="inline-code">reply.InputHint = InputHints.AcceptingInput;</span>,不應該使用 <span class="inline-code">IgnoringInput</span> 或 <span class="inline-code">ExpectingInput</span>。<br />
如果使用非 <span class="inline-code">AcceptingInput</span> 會發生錯誤,因爲系統會一直等待您的下一個訊息造成音樂不會播放整個卡住。<br />
<br />
需注意 Cortana 處理 Audio Card 訊息的方式:<br />
<ol><li>Cortana 不會主動通知 Skill 歌曲或是歌單播放完畢</li>
<li>回傳的 message 包含 speech,Cortana 會讓 audio stream 到背景播放並降低音量,直到完成 speaking</li>
<li>利用 Windows/iOS/Android 上面的 Cortana App 測試播放 audio,需要保持 Cortana 一直被開著,如果退到背景,音樂就會結束</li>
<li>在 Windows 上,在用戶呼叫您的 Skill 到結束對話之前,Cortana 會傳送用戶的句子到您的 Skill。結束對話後,Cortana 離開您的 Skill 繼續處理其他用戶輸入的句子</li>
<li>Speaker-only devcies 上,Cortana 會處理所有用戶輸入的句子就算再播放歌曲期間,所以在播放歌曲期間想要讓 Skill 收到用戶輸入的句子,用戶必須引用您的 Skill 說: "Ask {skill invocation name} to ..."</li>
</ol><br />
接著説明一些 commands 從 Skill 角度與 User 角度要怎麽控制 Audio:<br />
<ul><li>User audio commands<div>下面指令只支援 speaker-only devices:<br />
<table border="1"><tbody>
<tr><td><b>Command</b></td><td><b>Description</b></td></tr>
<tr><td>Stop</td><td>Stops the audio streaming.</td></tr>
<tr><td>Pause</td><td>Pauses the audio streaming.</td></tr>
<tr><td>Resume</td><td>Resumes the audio streaming.</td></tr>
<tr><td>Next</td><td>Play the next track in a playlist.</td></tr>
<tr><td>Previous</td><td>Play the previous track in a playlist.</td></tr>
</tbody></table>因爲是直接控制 Cortana 所以需要利用 "Hey Cortana" 起頭,例如:"Hey Cortana Pause"。Cortana 會利用這些指令控制正在播放的 audio,這些指令不會傳給 Skill。<br />
如果 device 有支援 Controller,例如:在 Windows/iOS/Android 上安裝的 Cortana App 也可以控制 (但是用語音指令無法控制),如下圖:<br />
<a href="https://3.bp.blogspot.com/-8Lil8MX4Ps0/WpJdDsp3lGI/AAAAAAAAA0s/MCdSZRmUMP8DiJcsWg55RjzHvjOhR0GGACLcBGAs/s1600/cortana_speaker_only_device.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="783" data-original-width="394" height="400" src="https://3.bp.blogspot.com/-8Lil8MX4Ps0/WpJdDsp3lGI/AAAAAAAAA0s/MCdSZRmUMP8DiJcsWg55RjzHvjOhR0GGACLcBGAs/s400/cortana_speaker_only_device.png" width="201" /></a></div></li>
<li>Getting the playback state<div>如果您的 Skill 是播放音訊,在 Cortana 傳送到您 Skill 的所有訊息,均會在訊息中的 <span class="inline-code">channelData</span> property 包含 <span class="inline-code">CurrentAudioInfo</span> 的物件。<br />
範例情境,例如想知道現在播放的歌曲名稱,可以說: "Hey Cortana, ask {skill invocation name} what's playing"。<br />
Skill 就能利用 <span class="inline-code">CurrentAudioInfo</span> 拿到現在播放的資訊,例如下面的程式:<br />
<pre class="code prettyprint"><code class="language-csharp">JObject valueObj = JsonConvert.DeserializeObject<jobject>(activity.ChannelData.ToString());
string url = "";
JObject currentAudioObject = JsonConvert.DeserializeObject<jobject>(valueObj["currentAudioInfo"]?.ToString() ?? "");
if(currentAudioObject!=null)
{
url = currentAudioObject["url"]?.ToString() ?? "";
}</jobject></jobject></code></pre>需注意 <span class="inline-code">CurrentAudioInfo</span> 只支援 speaker-only device。<br />
同樣地,上面提到 Cortana 播放歌曲或是歌單完畢之後不會通知 Skill 已經沒有下一首,這個時候,我們可以訂一個特定的 utterance,例如:"Hey Cortana, ask {skill invocation name} to keep going",利用 <span class="inline-code">CurrentAudioInfo</span> 知道現在播放的最後一首歌曲是什麽,再往下抓取其他的歌曲來播放。<br />
那麽 <span class="inline-code">channelData</span> 擁有那些資訊,可以參考如下:<br />
<pre class="code prettyprint"><code>{
"skillId": "18534e42-4a9b-4eb6-b788-6cae11ebe544",
"skillProductId": "3b169322-d53c-4286-bf0c-bed462fdead",
"isDebug": false,
"currentAudioInfo": {
"url": "https://xxx.azurewebsites.net/32fi_2o1.mp3"
}
}</code></pre>更多可以參考 <a href="https://docs.microsoft.com/zh-tw/cortana/skills/cortana-channel-data" target="_blank">Get Cortana's channel data</a>。</div></li>
<li>Skill audio commands<div>Skill 在某些情況下會傳送控制指令給 Cortana (例如:發現要播放的音訊已經不存在,需要請 Cortana 不要再來詢問播放)。<br />
主要設定回傳訊息的 Type 為 <span class="inline-code">ActivityTypes.Event</span>,並給予 <span class="inline-code">media/stop</span> 的 Name,如下:<br />
<pre class="code prettyprint"><code class="language-csharp">Activity reply = activity.CreateReply();
reply.Type = ActivityTypes.Event;
reply.Name = "media/stop";
reply.InputHint = InputHints.AcceptingInput;
await connector.Conversations.ReplyToActivityAsync(reply);</code></pre>如果對象是 speaker-only devices 建議不要送這指令,除非現在播放歌曲的 Skill 就是您自己,不然會造成使用上的不一致。</div></li>
</ul><br />
以上是介紹如何使用 Audio Card 讓 Cortana Skill 支援音訊播放的功能。<br />
您也許會問,<u>因爲 Cortana Skill 只有允許播放 internet accessible 與 HTTPS 是否代表需要完整提供播放的網址呢 (例如: https://xxx.com/audio/1.mp3)</u>?<br />
答案是<b>不需要</b>的。<br />
<br />
那可以怎麽做呢?<br />
<ol><li>在您的 Skill 多一個 Controller,命名為: RedirectAudioController,它負責處理 Cortana 請求 Audio stream 的回傳内容</li>
<li>在建立 Audio Card attachment 的 url 改指定為 <span class="inline-code">https://localhost:3979/api/RedirectAudio/{query string}</span>。<div>localhost 請換成對的 domian;<br />
query string: 請利用加入 Bot Framework 的資訊,讓 RedirectAudioController 處理時可以做一些簡單的判斷跟處理;<br />
參考下面的程式範例:<br />
<pre class="code prettyprint"><code class="language-csharp">public static IMessageActivity GenerateAudioCards(IDialogContext context, List<TrackData> tracks)
{
// 把目前 converstaion 的資訊保存起來,做 Controller 驗證用
var conversationReference = context.Activity.ToConversationReference();
string stateToken = UrlToken.Encode(conversationReference);
// 準備要回復的 message
var reply = context.MakeMessage();
reply.Speak = reply.Text = "Prepare play music";
reply.InputHint = InputHints.AcceptingInput;
reply.Attachments = new List<attachment>();
foreach (var item in tracks)
{
// 準備 query string
string queryString = $"state={stateToken}&track={HttpUtility.UrlEncode(item.Id)}";
byte[] stringData = Encoding.UTF8.GetBytes(queryString);
queryString = Convert.ToBase64String(stringData);
var album = item.Album;
// 填寫 subTitle 與 image,但是 Cortana 會忽略 (期待之後會加入)
string subTitle = $"{album.Name} {album.Artist.Name}";
ThumbnailUrl image = new ThumbnailUrl(album.Image.Url, item.Name);
// 把建立好的 query string 加入要處理的 Controller
var mediaUrls = new List<mediaurl>();
mediaUrls.Add(new MediaUrl($"http://localhost:3979/api/RedirectAudio/{queryString}", queryString));
var audioCard = new AudioCard(item.Name, subTitle, subTitle, image, mediaUrls, null, false, false, true);
reply.Attachments.Add(audioCard.ToAttachment());
}
return reply;
}</code></pre></div></li>
<li>最後 RedirectAudioController 拿到真正的 mp3 url 之後,利用 <a href="https://www.google.com.tw/search?q=HttpResponseMessage&oq=HttpResponseMessage&aqs=chrome..69i57j0l5.280j0j7&sourceid=chrome&ie=UTF-8" target="_blank">HttpResponseMessage</a> 重新請 Cortana 做 redirect。<div>如果您覺得再 redirect 一次太麻煩,也可以直接把歌曲的 byte array 寫到 <a href="https://www.google.com.tw/search?q=HttpResponseMessage&oq=HttpResponseMessage&aqs=chrome..69i57j0l5.280j0j7&sourceid=chrome&ie=UTF-8" target="_blank">HttpResponseMessage</a>。<br />
參考下面的程式範例:<br />
<pre class="code prettyprint"><code class="language-csharp">public async Task<httpresponsemessage> Get(string id)
{
try
{
// 將 query string 還原
var bytes = Convert.FromBase64String(id);
string input = Encoding.UTF8.GetString(bytes);
Dictionary<string, string> keyValuePairs = input.Split('&').Select(value => value.Split('=')).ToDictionary(pair => pair[0], pair => pair[1]);
// 從 state 參數轉換為原本的 ConversationReference
ConversationReference conversationReference = UrlToken.Decode<conversationreference>(keyValuePairs["state"]);
string trackId = HttpUtility.UrlDecode(keyValuePairs["track"]);
MusicProvider clientAPI = new MusicProvider(accessToken);
var ticketResult = await clientAPI.GetAudioStreamAsync(trackId);
if (ticketResult.Error != null)
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
// 使用 Rediret 的機制通知 Cortana 下載歌曲
var url = ticketResult.Content.URL;
var response = new HttpResponseMessage(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(url);
// 或是選擇使用 直接給與 byteArrary
//WebClient client = new WebClient();
//var fileStream = client.DownloadData(url);
//var stream = new MemoryStream();
//// processing the stream.
//stream.Write(fileStream, 0, fileStream.Length);
//var response = new HttpResponseMessage(HttpStatusCode.OK)
//{
// Content = new ByteArrayContent(stream.ToArray())
//};
//response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
//{
// FileName = $"0{id}.mp3"
//};
//response.Content.Headers.ContentType = new MediaTypeHeaderValue("audio/mp3");
return response;
}
catch (Exception ex)
{
return new HttpResponseMessage(HttpStatusCode.NotFound);
}
}</code></pre></div></li>
</ol><br />
[補充]<br />
<ul><li>開發 Cortana Skill 現在分成兩種方式: Bot Framework 與 Knowledge Store 線上設計與開發,如果在 Knowledge Store 上怎麽回傳 audio card 可以參考: <a href="https://help.knowledge.store/system_concepts/renderers_templates/index.html?highlight=audio%20streaming#adding-an-audio-card" target="_blank">audio in renderers</a></li>
<li>有一些已知的問題可以參考:<a href="https://docs.microsoft.com/en-us/cortana/skills/audio-streaming#known-issues" target="_blank">Know issues</a></li>
</ul>======<br />
在 Smart Speaker 最頻繁被使用的就是播放歌曲,剛好自己入手了 <a href="https://www.harmankardon.com/invoke.html" target="_blank">Harman Kardon Invoke</a> (目前唯一支援 Cortana 的產品),<br />
搭配本篇介紹的内容就可以讓 Invoke 唱歌,真的非常有趣。如果您沒有 Invoke 也可以利用 Windows 10 IoT 的方式整合 Cortana Skill。<br />
<br />
<b>References</b>:<br />
<ul><a href="https://docs.microsoft.com/zh-tw/cortana/skills/cortana-channel-data" target="_blank">Get Cortana's channel data</a>
<li><a href="https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-add-rich-card-attachments" target="_blank">Add rich card attachments to messages</a></li>
<li><a href="https://docs.microsoft.com/en-us/adaptive-cards/get-started/bots" target="_blank">Adaptive Cards for Bot Developers</a></li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.connector.audiocard?view=botconnector-3.12.2.4" target="_blank">AudioCard Class</a></li>
<li><a href="https://docs.microsoft.com/en-us/dotnet/api/microsoft.bot.connector.videocard?view=botconnector-3.12.2.4" target="_blank">VideoCard Class</a></li>
<li><a href="https://docs.microsoft.com/en-us/bot-framework/dotnet/bot-builder-dotnet-activities" target="_blank">Activities overview</a></li>
<li><a href="https://mspoweruser.com/cortana-skills-kit-now-supports-adaptive-cards/">Cortana Skills Kit now supports Adaptive Cards</a></li>
<li><a href="http://www.garypretty.co.uk/2017/05/10/creating-testing-a-cortana-skill-with-microsoft-bot-framework/" target="_blank">Creating and testing a Cortana Skill with Microsoft Bot Framework</a></li>
<li><a href="https://docs.microsoft.com/en-us/adaptive-cards/display/libraries/uwp" target="_blank">UWP SDK - Adaptive Card</a></li>
<li><a href="https://docs.microsoft.com/en-us/adaptive-cards/" target="_blank">Adaptive Card overview</a></li>
<li><a href="https://www.cnet.com/how-to/amazon-echo-the-complete-list-of-alexa-commands/" target="_blank">The complete list of Alexa commands so far</a><br />
</li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-12718022580332663592018-01-19T17:15:00.000+08:002018-01-19T17:15:21.582+08:00UWP - 開發 Xbox App 處理 TV-safe<a href="https://poumason.blogspot.tw/2017/11/uwp-xbox-app-xy-navigation.html" target="_blank">UWP - 開發 Xbox App 處理 XY navigation</a> 重點接受 XY navigation 與 Focus engagement,這篇將繼續介紹 <a href="https://docs.microsoft.com/zh-tw/windows/uwp/design/devices/designing-for-tv#tv-safe-area" target="_blank">Designing for Xbox and TV</a> 後面幾個重要内容。<br />
<a name='more'></a><br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#ui-element-sizing" target="_blank">UI element sizing</a><div>由於 10-foot environment 加上用戶坐的比較遠,並利用 remote control / gamepad 在操作,需避免讓畫面太混亂,讓用戶能輕易的導覽到需要的元素 (<b>簡潔的畫面是重點</b>) 。<br />
<ol><li>scale factor and adaptive layout<div>在 PC/Mobile 可利用 settings > system > display 去調整 scale factor,檢查 UI 在不同 DPI 時是否有跑掉的部分 (Xbox 不支援)。以 Xbox 爲例,預設在 <b>XAML 是 200%</b> 比例 (HTML app 是 150%),就可以從 100% 加以調整或是根據 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/layout/screen-sizes-and-breakpoints-for-responsive-design" target="_blank">adaptive techniques</a> 來調整畫面。<br />
開發 Xbox App 只需要在一個解析度:1920x1080,不論用戶使用再好的 TV 還是會走到 1080p 的比例,App 也會被系統自動放大到適合的 size。</div></li>
<li>content density<div>設計 UI 時要注意用戶使用 remote control / gamepad 不像 touch/mouse 這樣容易,所以物件的大小(Sizes of UI controls)與點擊的次數(Number of clicks)是很重要的。如下:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/button-100-200.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="337" data-original-width="611" height="220" src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/button-100-200.png" width="400" /></a><br />
確保元件在遠距離也能被看到,需把 focus 做的明顯。<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/six-clicks.png" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" data-original-height="450" data-original-width="800" height="225" src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/six-clicks.png" width="400" /></a><br />
點擊次數代表用戶要完成或選擇一個元素需要點擊多少次數才能達到,如上圖要點擊 6 次才能選擇到,可以重新設計出最短距離來完成,詳細可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#path-of-least-clicks" target="_blank">Path of least clicks</a>。</div></li>
<li>text sizes<div>爲了確保文字能在固定距離也能被看到,可參考 <b>主要文字與閲讀内文,使用: 15 epx minimum</b>;<b>非主要文字或是説明内容,使用 12 epx minimum</b>。</div></li>
<li>Opting out of scale factor<div>微軟建議我們使用 advantage of scale factor 去維持畫面比例的縮放,但是您也可以選擇永遠維持在 100% 比例 (當關閉 advantage of scale factor 就只能設定 100%)的呈現,如下:<br />
<pre class="code prettyprint"><code class="language=csharp">bool result = Windows.UI.ViewManagement.ApplicationViewScaling.TrySetDisableLayoutScaling(true);</code></pre>如果成功關閉會得到 result = true,更多訊息可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/disable-scaling" target="_blank"> How to turn off scaling</a>。需注意計算比例如果是 XAML 要乘 2.0, 如果是 HTML 要乘 1.5。</div></li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#tv-safe-area" target="_blank">TV-safe area</a><div>由於歷史與技術原因,並非所有電視都把内容全部顯示到螢幕的邊緣。<br />
預設 UWP 自動避免在 unsafe 區域顯示内容,在 unsafe 的部分會顯示頁面的背景圖或顔色。如下圖:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-unsafe-area.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-unsafe-area.png" width="700" /></a><br />
可以看到邊界就留了 (48,27,48,27),需注意隨著電視調整解析度顯示的效果有所不同,但是不會離這個數值太多。<br />
另外可設定 <b>Theme color</b> 或是 <b>Image</b>去調整 Page 邊界的顯示:<br />
<pre class="code prettyprint"><code class="language-csharp">// 直接使用 ThemeResource 的内建主題
<Page x:Class="MySample1.MainPage" Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"/>
// 設定圖片為背景
<Page x:Class="MySample1.MainPage" Background="\Assets\AppBackground.png"/>
</code></pre>在還沒有調整前將 App deploy 到 Xbox 會看到類似如下的圖:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-area.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-area.png" width="700" /></a><br />
看起來會感覺好像盒子裏面又有盒子一樣的曡層效果,這個是可以調整的。微軟建議使用 <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.scrollviewer.aspx" target="_blank">ScrollViewers</a>, <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/navigationview" target="_blank">nav panes</a>或 <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.commandbar.aspx" target="_blank">CommandBars</a> 來延伸内容到邊界。避免因爲邊界問題(48,27,48,27)造成内容顯示被電視截斷或是看不到。<br />
下面介紹幾個調整方式:<br />
<ol><li>Core window bounds<div>UWP 爲了 10-foot experience,直接設定 window bound 是最簡單的方式。直接在 <span class="inline-code">App.xaml.cs</span> 的 <span class="inline-code">OnLaunched</span> 加入下面的 code:<br />
<pre class="code prettyprint"><code class="language-csharp">Windows.UI.ViewManagement.ApplicationView.GetForCurrentView().SetDesiredBoundsMode
(Windows.UI.ViewManagement.ApplicationViewBoundsMode.UseCoreWindow);</code></pre>加入這段 code 之後,app 的 window 會延伸到 TV 的邊界:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/core-window-bounds.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/core-window-bounds.png" width="700" /></a><br />
我們就需要調整所有可以互動的 UI 元件到 TV-safe area。</div></li>
<li>Pane backgrounds<div>由於 navigation panes 接近 TV-safe area 邊界,避免漏出畫面邊界綫,可以把 navigation panes 的背景色設定特定顔色後,再設定 margins 讓畫面呈現在 TV-safe area 裏面,如 code:<br />
<pre class="code prettyprint"><code class="language-csharp"><SplitView x:Name="RootSplitView"
Margin="48,0,48,0">
<SplitView.Pane>
<ListView x:Name="NavMenuList"
ContainerContentChanging="NavMenuItemContainerContentChanging"
ItemContainerStyle="{StaticResource NavMenuItemContainerStyle}"
ItemTemplate="{StaticResource NavMenuItemTemplate}"
ItemInvoked="NavMenuList_ItemInvoked"
ItemsSource="{Binding NavMenuListItems}"/>
</SplitView.Pane>
<Frame x:Name="frame"
Navigating="OnNavigatingToPage"
Navigated="OnNavigatedToPage"/>
</SplitView></code></pre>效果如下圖:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-areas-2.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-areas-2.png" width="700" /></a><br />
如果您的 App 有用到 <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.commandbar.aspx" target="_blank">CommandBar</a> 的話,可藉由設定 background 顔色,例如:如果是 top command bar 可以用透明色(transparent)讓它跟 Page 同色效果像是浮在畫面上;如果是 below command bar 就填滿顔色讓它變成是底部的感覺。<br />
</div></li>
<li>Scrolling ends of lists and grids<div>使用 ListView 或 GridView 元件是常用到的,他們可以裝比畫面顯示更多的内容,此時會利用 gamepad 水平移動到最右邊移動到最後一個,或是垂直移動到底部的最後一個。<br />
但是移動時 focus 在 TV-safe area 也是要特別處理的,才能做到如下的圖,避開 focus 到一個被畫面切掉的 UI:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/scrolling-grid-focus.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/scrolling-grid-focus.png" width="700" /></a><br />
由於 UWP 具有將 focus 顯示于 <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.viewmanagement.applicationview.visiblebounds.aspx" target="_blank">VisibleBounds</a> 内部的功能,因此需要去設定容器(ListView 或 GridView) <a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.itemspresenter.aspx" target="_blank">ItemsPresenter</a> 的 margin,<br />
<pre class="code prettyprint"><code class="language-csharp">// 建立一個 Style
<Style x:Key="TitleSafeListViewStyle"
TargetType="ListView">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListView">
<Border BorderBrush="{TemplateBinding BorderBrush}"
Background="{TemplateBinding Background}"
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer x:Name="ScrollViewer"
TabNavigation="{TemplateBinding TabNavigation}"
HorizontalScrollMode="{TemplateBinding ScrollViewer.HorizontalScrollMode}"
HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
IsHorizontalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsHorizontalScrollChainingEnabled}"
VerticalScrollMode="{TemplateBinding ScrollViewer.VerticalScrollMode}"
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"
IsVerticalScrollChainingEnabled="{TemplateBinding ScrollViewer.IsVerticalScrollChainingEnabled}"
IsHorizontalRailEnabled="{TemplateBinding ScrollViewer.IsHorizontalRailEnabled}"
IsVerticalRailEnabled="{TemplateBinding ScrollViewer.IsVerticalRailEnabled}"
ZoomMode="{TemplateBinding ScrollViewer.ZoomMode}"
IsDeferredScrollingEnabled="{TemplateBinding ScrollViewer.IsDeferredScrollingEnabled}"
BringIntoViewOnFocusChange="{TemplateBinding ScrollViewer.BringIntoViewOnFocusChange}"
AutomationProperties.AccessibilityView="Raw">
<ItemsPresenter Header="{TemplateBinding Header}"
HeaderTemplate="{TemplateBinding HeaderTemplate}"
HeaderTransitions="{TemplateBinding HeaderTransitions}"
Footer="{TemplateBinding Footer}"
FooterTemplate="{TemplateBinding FooterTemplate}"
FooterTransitions="{TemplateBinding FooterTransitions}"
Padding="{TemplateBinding Padding}"
Margin="0,27,0,27"/>
</ScrollViewer>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
// 套用在 ListView 或是 GridView 讓 ItemsPresenter 能保留 TV-safe area 需要的間距
<ListView Style="{StaticResource TitleSafeListViewStyle}" />
</code></pre></div></li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#colors" target="_blank">Colors</a><div>預設 UWP 會自動把 App 的顔色調整到適合 <b>TV-safe colors</b> 的範圍,讓 App 在任何一台電視看起來是正常的。當然我們也可以修改成自己喜歡的顔色,但是要注意並不是什麽顔色都適合在電視上顯示。<br />
最簡單是使用 <b>Application theme</b>,讓 App 根據系統設定的主題來套用顔色,但是很可惜 Xbox 爲了確保顔色都能在任何電視呈現,預設使用黑色(dark)的主題。<br />
既然主題色無法調整,在 Xbox 還有 <b>Accent color</b> 可以控制,需注意 Accent color 是跟著 User 不是系統,因爲 Xbox 可以設定多個 User 他們分別可以使用不同的顔色,這個跟 PC/Mobile 不一樣。<br />
透過 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/style/color" target="_blank">Color themes</a> 看到 Xbox 推薦的 Accent color,App 中可在 brush / colors resources 中使用它們,例如:SystemControlForegroundAccentBrush 或是 SystemAccentColor,或是直接使用 <span class="inline-code">UIColorType.Accent</span> 選擇要用的顔色。<br />
<br />
<b>TV-safe color</b><br />
由於電視會有色差(Color variance among TVs),因此設計出來的顔色有可能與你在顯示器不一樣,建議不要用顔色做内容的差異,因爲會容易造成用戶混肴。<br />
既然如此,文章裏就提供了 TV-safe color 讓我們參考,如下圖:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-colors-2.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/tv-safe-colors-2.png" width="700" /></a><br />
電視不會處理極端的顔色,因爲它會造成顔色的帶狀效應 (odd banded effect) 或是某些顔色就消失了,甚至有些極端顔色會變成一樣的顔色。<br />
因爲不同電視廠商處理顔色的方式不同,因此,RGB 顔色在 16-235(或 10-EB 16 進位) 範圍内是最安全的。<br />
但是有一個好消息,就是更新到 Fall Creators Update,Xbox 會自動處理 color full range 到 TV-safe range。<br />
<b>注意</b><br />
<ol><li>如果您開發的 App 使用到 DirectX 11 或 DirectX 12 來畫 UI 或影片,必須利用 <a href="https://msdn.microsoft.com/library/windows/desktop/dn903676" target="_blank">IDXGISwapChain3::SetColorSpace1</a> 設定顔色空間,讓系統知道是否要縮放顔色。</li>
<li>如果 played back 利用 <a href="https://msdn.microsoft.com/library/windows/desktop/ms694197" target="_blank">Media Foundation</a> 在 TV-safe color 裏面播放影片時不會有顔色縮放效果。</li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/style/sound" target="_blank">Sound</a><div>在 10-foot experience 中聲音播放扮演很重要的角色,例如:用戶利用 gamepad 導覽到控制項會自動發出 focus 的音效。<br />
可以幫助用戶更快找到自己目前的動作位置。在 UWP 程式在 Xbox 運行時會自動打開控制項的音效。<br />
開啓音效原件是: <a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.elementsoundplayer" target="_blank">ElementSoundPlayer</a>,利用下面的 code 説明:<br />
<pre class="code perttyprint"><code class="language-csharp">// 設定開啓控制項的音效,如果是在 Xbox 預設是 ElementSoundPlayerState.Auto
ElementSoundPlayer.State = ElementSoundPlayerState.On;
// 設定音量
ElementSoundPlayer.Volume = 0.5;
// XAML sample
<Button Name="ButtonName" Content="More Info" ElementSoundMode="Off"/>
// in code , ElementSoundKind 有多種可用的内建音效
ElementSoundPlayer.Play(ElementSoundKind.Invoke);
</code></pre>詳細可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/style/sound" target="_blank">Sound</a>。<br />
</div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#guidelines-for-ui-controls" target="_blank">Guidelines for UI controls</a><div>文件裏提到幾個在 10-foot experience (on TV) 時需要注意的元件:<br />
<ol><li>Pivot control<div>Pivot 提供快速切換到不同 headers 或是 tabs 下的内容,會有 underline 標記現在在那個 tab。<br />
可以藉由設定 <span class="inline-code"><a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.pivot.isheaderitemscarouselenabled.aspx" target="_blank">Pivot.IsHeaderItemsCarouselEnabled</a> = true</span> 去保持用戶最後選擇的 tab,而不用每次回畫面都要重第一個開始選擇。<br />
這個對 TV 上的操作很重要,如果遇到 tabs 過多時,這樣可幫助用戶繼續上一步任務不需要重新選擇。更多可參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/tabs-pivot" target="_blank">Tabs and pivots.</a>。</div></li>
<li>Navigation pane<div>navigation pane(或是大家說的漢堡按鈕)在 UWP app 很常被使用,通常 pane 一開始都是被收起來節省畫面空間,用戶去點擊漢堡才會打開 pane。<br />
利用 touch/mouse 很容易,但是 gamepad/remote control 就很難去選到那個位置。<br />
因此官方建議處理 gamepad 上面的 <b>view button</b>,讓用戶直接按下那個按鈕就可以開啓 pane,可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/input/managing-focus-navigation#split-view-code-sample" target="_blank">Managing focus navigation</a>。<br />
</div></li>
<li>CommandBar<div><a href="https://docs.microsoft.com/en-us/uwp/api/Windows.UI.Xaml.Controls.CommandBar#Windows_UI_Xaml_Controls_CommandBar_DefaultLabelPosition" target="_blank">CommandBar 預設按鈕的文字是顯示在下方</a>,這樣的顯示方式不適合在 10-foot experience,因爲用戶距離太遠不容易看到下面的文字。<br />
因此,建議設定為 <span class="inline-code">CommandBarDefaultLabelPosition.Right</span> 呈現如下的效果:<br />
<a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/commandbar.png" target="_blank"><img src="https://docs.microsoft.com/en-us/windows/uwp/design/devices/images/designing-for-tv/commandbar.png" width="300" /></a><br />
幫助用戶能更清楚看到該按鈕的説明。</div></li>
<li>Tooltip<div><a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.tooltip.aspx" target="_blank">Tooltip</a> 比較常見是在 mouse over 的時候會顯示,但是 Xbox 使用 gamepad/remote control 操作其實不需要,因爲用戶很快就會 navigation 過去。建議只對特定的元件在做 tooltip 就好,例如: Nested UI elements。</div></li>
<li>Button styles<div>預設 Button style 在 10-foot experience 可以做一些調整,讓它被 focus 的時候可以更明顯,更多説明可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/buttons" target="_blank">Button</a>。</div></li>
<li>Nested UI elements<div>XY navigation 幫助 focus 設定,讓用戶能夠知道目前 focus 的位置。如果您的 App 用到 <span class="inline-code">ContextFlyout</span> 建議在顯示 Flyout item 時自動設定 focus 到裏面的項目,並且處理 <b>B button or Back button</b> 時,能夠正確回到顯示 Flyout 之前的元件。詳細可以參考 <a href="https://docs.microsoft.com/en-us/windows/uwp/design/controls-and-patterns/nested-ui" target="_blank">Nested UI in list items.</a>。<br />
另外我自己比較建議在 Xbox 或是 TV 上遇到 Nested UI in list items 的設計,搭配 gamepad 按鈕的 hint,讓減少用戶需要 navigation 的次數。</div></li>
<li>MediaTransportControls<div><a href="https://msdn.microsoft.com/library/windows/apps/windows.ui.xaml.controls.mediatransportcontrols.aspx" target="_blank">MediaTransportControls</a> element 幫助用戶控制播放媒體(play, pause, ...)。這個功能支援 <a href="https://msdn.microsoft.com/library/windows/apps/Windows.UI.Xaml.Controls.MediaPlayerElement.aspx" target="_blank">MediaPlayerElement</a>,但是它需要在 Windows 10, version 1607 and later,如果是比較舊的版本請使用 <span class="inline-code">MediaElement 的 TransportControls</span>。<br />
MediaTransportControls 具有兩種樣式 one-row 與 two-row,在 Xbox 上面較適合 two-row 如下圖:<br />
<a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls/images/mediatransportcontrols_anatomy.png" target="_blank"><img src="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.controls/images/mediatransportcontrols_anatomy.png" width="600" /></a><br />
設定方式:<br />
<pre class="code prettyprint"><code class="xml"><MediaPlayerElement x:Name="mediaPlayerElement1" Source="Assets/video.mp4" AreTransportControlsEnabled="True">
<MediaPlayerElement.TransportControls>
<MediaTransportControls IsCompact="False"/>
</MediaPlayerElement.TransportControls>
</MediaPlayerElement></code></pre></div></li>
<li>Search experience<div>搜尋内容在 10-foot experience 是很常使用的功能。如果您的 App 也有提供這樣的功能,微軟建議您加入 gamdped 上 <b>Y button</b> 的處理,讓用戶按下 Y 自動進去搜尋模式。<br />
如果您的 App 已經預期用 Y 按鈕做其他事情,那您可以使用 <b>Segoe Xbox MDL2 Symbol(only for Xbox)</b> 字形在 Button 或是 TextBlock 畫上搜尋符號 (<span class="inline-code">&#xE3CC;</span> for XAML app;<span class="inline-code">\E426</span> for HTML app)。<br />
當然,如果希望其他設備也可以使用的話,我建議你改用下面的 code:<br />
<pre class="code prettyprint"><code class="xml"><TextBlock Text="&#xE094;" FontFamily="Segoe MDL2 Assets" /></code></pre>另外建議搜尋畫面在進入時自動 focus 到輸入框,讓鍵盤自動升起搭配 AutoSuggestTextBox 把建議字一並顯示,加速用戶找到需要的内容。<br />
</div></li>
</ol></div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/design/devices/designing-for-tv#custom-visual-state-trigger-for-xbox" target="_blank">Custom visual state trigger for Xbox</a><div>搭配自定義的 visual state trigger 讓用戶在操作上感覺 UI 的變化。<br />
例如:繼承 <span class="inline-code"><a href="https://docs.microsoft.com/en-us/uwp/api/windows.ui.xaml.statetriggerbase" target="_blank">StateTriggerBase</a></span> 判斷是否為 Xbox 設備,如果是做 UI 的調整,如下面的 code: <br />
<ol><li>自定義一個 StateTrigger:<pre class="code prettyprint"><code class="language-csharp">class DeviceFamilyTrigger : StateTriggerBase
{
private string _currentDeviceFamily, _queriedDeviceFamily;
public string DeviceFamily
{
get
{
return _queriedDeviceFamily;
}
set
{
_queriedDeviceFamily = value;
// Windows.Xbox
_currentDeviceFamily = AnalyticsInfo.VersionInfo.DeviceFamily;
// 檢查如果是的話回傳 true, 代表觸發 trigger
SetActive(_queriedDeviceFamily == _currentDeviceFamily);
}
}
}</code></pre></li>
<li>在 XAML 中定義 <span class="inline-code">VisualState.StateTrigger</span> 使用自定義的 <span class="inline-code">triggers:DeviceFamilyTrigger</span> 是 Xbox 設備,如下範例:<br />
<pre class="code prettyprint"><code class="xml"><VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState>
<VisualState.StateTriggers>
<triggers:DeviceFamilyTrigger DeviceFamily="Windows.Xbox"/>
</VisualState.StateTriggers>
<VisualState.Setters>
<Setter Target="RootSplitView.OpenPaneLength"
Value="368"/>
<Setter Target="RootSplitView.CompactPaneLength"
Value="96"/>
<Setter Target="NavMenuList.Margin"
Value="0,75,0,27"/>
<Setter Target="Frame.Margin"
Value="0,27,48,27"/>
<Setter Target="NavMenuList.ItemContainerStyle"
Value="{StaticResource NavMenuItemContainerXboxStyle}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups></code></pre></li>
</ol></div></li>
</ul>[<b>補充</b>] <br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/tailoring-for-xbox" target="_blank">Xbox best practices</a><div>開發時的 cookbook。</div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/ves-on-xbox" target="_blank">Using Speech to Invoke UI Elements</a><div>可以利用語音的方式控制 App 裏的 UI Elements</div></li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/system-resource-allocation" target="_blank">System resources for UWP apps and games on Xbox One</a><div>一個 App 最大記憶體量是 1G,如果退到背景模式只剩下 128 MB,如果是做音樂 App 特別注意避免 App 被系統停止。</div></li>
<li><a href="https://docs.microsoft.com/en-us/uwp/extension-sdks/uwp-limitations-on-xbox" target="_blank">UWP features not yet supported on Xbox</a><div>有些 APIs 不一定又支援 Xbox,這篇要記得看。</div></li>
<li>一次設定 TV-safe size 與 TV-safe color:<br />
<pre class="code prettyprint"><code class="language-csharp">void OnLaunched(LaunchActivatedEventArgs e)
{
if (App.IsXbox())
{
// use TV colorsafe values
this.Resources.MergedDictionaries.Add(new ResourceDictionary
{
Source = new Uri("ms-appx:///TvSafeColors.xaml")
});
// remote TV safe areas
ApplicationView.GetForCurrentView().SetDesiredBoundsMode(ApplicationViewBoundsMode.UseCoreWindow);
}
}
</code></pre></li>
</ul>======<br />
開發 Xbox 上面的應用畫面真的需要重新設計符合電視的操作會比較好,相對地,畫面的不同建議 ViewModel 也要重新設計,<br />
減少原本因爲要通用在 Desktop/Mobile 上的使用造成資源的浪費。 <br />
希望這兩篇的介紹可以幫忙大家更瞭解 Xbox 開發應用需要注意的地方。 <br />
<br />
<b>References</b>: <br />
<ul><li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/" target="_blank">UWP on Xbox One</a>(重要)</li>
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/whats-new" target="_blank">What's new for developers in the latest update of UWP on Xbox One</a>/li><br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/input-and-devices/designing-for-tv#mouse-mode" target="_blank">Designing for Xbox and TV</a>(重要)</li><br />
<br />
<li><a href="http://go.microsoft.com/fwlink/p/?LinkID=760755" target="_blank">UWP features that aren't yet supported on Xbox</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/how-to-disable-mouse-mode" target="_blank">disable mouse mode</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/input-and-devices/designing-for-tv#focus-visual" target="_blank">Focus visual</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/input-and-devices/designing-for-tv#tv-safe-area" target="_blank">TV-safe area</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/turn-off-overscan" target="_blank">How to draw UI to the edge of the screen</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/input-and-devices/designing-for-tv#colors" target="_blank">Read Colors to understand how to make your app look great to everybody!</a></li><br />
<br />
<li><a href="https://channel9.msdn.com/Events/Build/2016/B883" target="_blank">Building Great Universal Windows Platform (UWP) Apps for Xbox</a></li><br />
<br />
<li><a href="https://channel9.msdn.com/Events/Build/2016/T651-R1" target="_blank">Adapt Your App for Xbox One and TV</a></li><br />
<br />
<li><a href="https://channel9.msdn.com/Events/Build/2016/L724-R1" target="_blank">UWP Development 1: Building an Adaptive UI</a></li><br />
<br />
<li><a href="https://channel9.msdn.com/Events/Build/2016/B888" target="_blank">Web Apps Beyond the Browser: Cross-Platform Meets Cross Device</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/system-resource-allocation" target="_blank">System resources for UWP apps and games on Xbox One</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/multi-user-applications" target="_blank">Introduction to multi-user applications</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/uwp-fiddler" target="_blank">How to use Fiddler with Xbox One when developing for UWP</a></li><br />
<br />
<li><a href="https://mva.microsoft.com/en-US/training-courses/developing-xbox-one-applications-16860?l=vk0fOPf9C_2006218965" target="_blank">Developing Xbox One Applications</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/xbox-apps/samples" target="_blank">UWP on Xbox One samples</a></li><br />
<br />
<li><a href="https://docs.microsoft.com/en-us/windows/uwp/input-and-devices/keyboard-interactions" target="_blank">Keyboard interactions</a></li><br />
<br />
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0tag:blogger.com,1999:blog-2649688415868412622.post-28764109564627947922018-01-01T16:55:00.001+08:002018-01-01T16:55:46.636+08:00開發小米 AI 上的自定义技能應用Smart Speaker(智能音箱) 的進步非常快速,例如:<a href="https://www.amazon.com/Amazon-Echo-And-Alexa-Devices/b?ie=UTF8&node=9818047011" target="_blank">Amazon Echo</a>, <a href="https://store.google.com/us/product/google_home?hl=en-US" target="_blank">Google Home</a>, <a href="https://www.microsoft.com/en-us/cortana/devices/invoke" target="_blank">Microsoft Invoke(Harman Kardon)</a> 都正在努力發展,中文的智能音箱就有小米,天貓...等,本篇將介紹怎麽開發在小米 AI 上面的 自定义技能。<br />
<a name='more'></a><br />
如果您研究過目前做語音控制雲端平臺的話,可以發現各家都有 Skills,例如 <a href="https://developer.amazon.com/alexa" target="_blank">Alexa</a>, <a href="https://docs.microsoft.com/zh-tw/cortana/skills/#pivot=start" target="_blank">Cortana</a>, <a href="https://assistant.google.com/" target="_blank">Google Assistant</a> 或是 LINE 的 <a href="https://clova.line.me/" target="_blank">Glova</a>,結構都是:<br />
<ul><li>一個 STT (Speech to ext) 的 Cloud Service 把從音箱收到的聲音轉成文字</li>
<li>一個 NLU (Natural language understanding) Cloud Platform 去理解使用者所説的内容,並分析出句子的意義與關鍵字比率</li>
<li>搭配分析出來的結果找到是否有對應處理的 Skills,將結果交給 Skills 去處理</li>
<li>Skill 是其他開發商自己在平臺注冊的服務,再根據收到的分析結果產生對應的任務與處理,最後再回傳給使用者</li>
</ul>同樣地,小米目前提供讓第三方開發者加入自己的技能,參考 <b><a href="https://shuidi.mi.com/documents/Home" target="_blank">水滴平台开发者文档</a></b> 瞭解交易時需要的參數與回傳的内容。<br />
我利用 <a href="https://docs.microsoft.com/zh-tw/dotnet/standard/net-standard" target="_blank">.NET Standard</a> 與 <a href="https://docs.microsoft.com/zh-tw/aspnet/core/getting-started" target="_blank">ASP.NET Core</a> 做了資料交易 SDK 與自定義技能的接口: <a href="https://github.com/poumason/Xiaomi-ai-skill-converter" target="_blank">Xiaomi-ai-skill-converter</a>。<br />
<br />
截取程式碼片段説明:<br />
<ol><li>Skill 收到小米 AI 請求時,可利用 <span class="inline-code">request.type</span> 理解用戶目前是要:喚醒/意圖/結束 的請求;<div>收到來自小米 AI 的 request, 内容大致如下:<br />
<pre class="code prettyprint"><code class="json">{
"version": "1.0",
"query": "開啓天氣查詢",
"session": { // 请求的上下文信息都放这
"session_id": "xxxxxxxxxxxxx",
"application": {
"app_id": "123"
}
},
"request": {
"type": 1,
"request_id": "tttttttttt",
"timestamp": 452453534523,
"event_type": "leavemsg.finished",
"event_property": {
"msg_file_id": "1231312312"
}
}
}</code></pre>可以利用下面的程式片段,把 JSON 轉成物件:<br />
<pre class="code prettyprint"><code class="language-csharp">using XiaomiAI.SDK.Models;
// POST api/values
[HttpPost]
public async Task<ResponseContent> Post([FromBody]string value)
{
using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8))
{
string requestJson = await reader.ReadToEndAsync();
var requestContent = JsonConvert.DeserializeObject<RequestContent>(requestJson);
switch (requestContent.Request.Type)
{
case RequestType.Intent:
// 代表用戶説了什麽内容,利用 request.query 抓到用戶講的内容
// query 不會有 NLU 的結果,需要搭配其他的 NLU 服務
break;
case RequestType.End:
// 代表要結束對話,記得標記 is_session_end = true;
break;
default:
// None, Wakeup, 預設用來詢問用戶想要在 Skill 做什麽
break;
}
}
}
</code></pre></div></li>
<li>搭配 Session 在每次的 response 加入參數:<div>如果 Skill 是允許多次對話且需要記錄上一次用戶説過的内容或是處理參數,可以搭配 response 中的 <span class="inline-code">session_attributes</span> 來記錄。<br />
session 會在 request 回傳時再帶回來 <span class="inline-code">session</span> 再送回來。<br />
下面是在 response 中加入 session 的内容:<br />
<pre class="code prettyprint"><code class="language-csharp">string displayText = "是的,幫你朗讀現在氣象";
string sessionAttributes = @"{ ""name"": ""pou"", ""age"": ""35"", ""email"": ""poumason@live.com""}";
return new ResponseContent
{
IsSessionEnd = false,
SessionAttributes = sessionAttributes,
Response = new ResponseData
{
// ToSpeak 與 ToDisplay 一定要給,來支援有屏幕跟沒有屏幕的設備
ToSpeak = new ToSpeakData
{
Type = ToSpeakType.TTS,
Text = displayText
},
ToDisplay = new PlainTextToDisplayData
{
Text = displayText
},
Directives = new List<IDirectiveData>
{
new AudioDirectiveData
{
AudioItem = new AudioItemData
{
Stream = new StreamData
{
Url = "http://xxxxx.mp3"
}
}
},
new TTSDirectiveData
{
TTSItem = new TTSItemData
{
Text = displayText
}
}
}
}
};
</code></pre></div></li>
</ol><br />
送上面的範例來看是不是非常簡單就可以做完一個 Skill 呢。下面説明幾個開發時遇到且要注意的事項:<br />
[<b>注意</b>]<br />
<ul><li><span class="inline-code">to_speak</span> / <span class="inline-code">to_display</span> 是一定要給的。<div><span class="inline-code">to_speak</span>:代表設備要念出的内容,目前只有 TTS;<br />
<span class="inline-code">to_display</span>:可顯示 text/html/native ui/widgets,其中 native ui / widgets 需要搭配 android 開發</div></li>
<li>每次交易小米 AI 音箱 <-> Skill 這段時間只有 <b>20 seconds</b> 回應時間,20 seconds 不是代表 Skill 處理時間,而是這個交易時間,所以 Skill 不能太複雜</li>
<li>水滴平臺有提供測試用的 App,盡量使用小米手機測試,因爲其他平臺可能會有一些小問題</li>
<li>建立自定義技能的名稱或是範例句子時,請使用<b>簡體中文</b>,繁體中文會找不到您的技能</li>
<li>Skill 必須是 https,它會被利用 POST 觸發,只能 HTTP status code 返回告訴 小米 API 最後 Skill 處理的狀態,例如: 500 是失敗,200 是成功</li>
</ul>======<br />
支援中文的智能音箱多數以中國爲主,支援台灣中文發音的廠商也不少,我更期待國際廠能支援中文。<br />
如果您沒有買過小米 AI 也許可以買一臺來玩看看,加入自己的服務會更有感覺。謝謝。<br />
<br />
<b>References</b><br />
<ul><li><a href="https://github.com/poumason/Xiaomi-ai-skill-converter" target="_blank">Xiaomi-ai-skill-converter</a></li>
<li><a href="https://shuidi.mi.com/" target="_blank">水滴平台</a></li>
<li><a href="https://shuidi.mi.com/case" target="_blank">水滴开放平台自定义技能</a></li>
<li><a href="https://shuidi.mi.com/documents/Home" target="_blank">水滴平台开发者文档</a></li>
<li><a href="https://docs.microsoft.com/zh-tw/cortana/skills/#pivot=start" target="_blank">Cortana Skills Documentation</a></li>
<li><a href="https://assistant.google.com/" target="_blank">Google Assistant</a></li>
<li><a href="https://developer.amazon.com/alexa" target="_blank">Amazon Alexa</a></li>
<li><a href="http://www.books.com.tw/products/0010761097" target="_blank">物聯網無限商機 產業概論x實務應用</a></li>
<li><a href="https://bot.tmall.com/" target="_blank">天貓精靈x1</a></li>
</ul>Pouhttp://www.blogger.com/profile/14366827626117087250noreply@blogger.com0