Windows Phone 8 - 操作External Storage API
在<Windows Phone 8 – Storages的轉變>與<Windows Phone 8 - 練習操作Storages>介紹StorageFolder與StoragFile,
接下來介紹在External Storage API的操作部分,這是WP8一個與過去WP 7.1很大不同的地方,因為在WP8之後,
支援將應用程式式、音樂、圖像…等放置於SD Card,因此,在開發應用程式時對於該Storage來源必要好好加
以研究一番,接下來將針對External Storage API加以說明。
操作External Storage中資料的概念,比較特別,需要應用程式本身「註冊特定的檔案類型」於WMAppManifest.xml,
包括:建立一個File Association與宣告處理的File Types,主要告知系統該應用程式可處理這些被它認識的檔案類型。
這概念與<Auto-launching apps using file and URI associations for Windows Phone 8>的是相同的。
那麼要操作External Storage (SD Card)的API主要分成以下幾個步驟:
A. 指定必要的Capability與註冊File Extensions:
在操作External Storage的API前,請先確認專案中的WMAppManifest.xml是否在<Capabilities />標籤裡加上了
<ID_CAP_REMOVABLE_STORAGE>特性,並且在程式裡using了Microsoft.Phone.Storage該命名空間,如下:
1: <Capabilities>
2: <Capability Name="ID_CAP_REMOVABLE_STORAGE" />
3: </Capabilities>
告知了該應用程式需具有讀取Remote Storage的特性後,接著要宣告預計要處理的檔案類型與File Association:
1: <!--
2: 定義要處理的File Association與宣告NavUriFragment的Token;
3: 這在實作urimapping類別時,非常重要;
4: 定義當出現要處理指定的File Type時,應用程式顯示的圖示;
5: 宣告指定的File Type為:;
6: -->
7: <FileTypeAssociation TaskID="_default" Name="GPX" NavUriFragment="fileToken=%s">
8: <Logos>
9: <Logo Size="small" IsRelative="true">Assets/Route_Mapper_Logo33x33.png</Logo>
10: <Logo Size="medium" IsRelative="true">Assets/Route_Mapper_Logo69x69.png</Logo>
11: <Logo Size="large" IsRelative="true">Assets/Route_Mapper_Logo176x176.png</Logo>
12: </Logos>
13: <SupportedFileTypes>
14: <FileType ContentType="application/gpx">.gpx</FileType>
15: </SupportedFileTypes>
16: </FileTypeAssociation>
上述定義了要承接的NavUriFragment、Logos與FileType,這些是實作應用程式具有File Associations必要的註冊宣告;
但Logos是選擇性的,可按照需求自行定義。
B. 繼承UriMapperBase修改由系統傳過來要負責處理的URI:
當應用程式註冊要處理某指定的檔案類型時,隨著用戶選擇該應用程式讀取該檔案時,系統會將檔案搭配對應的URI轉發
至應用程式。此時,應用程式需要有對應的URI Mapper來負責將收到的URI轉向負責的Page,進一步讀取與呈現檔案的內容。
了解這流程之後 ,往下看程式範例:
(B-1). 繼承UriMapperBase,實作File association type的轉向;
1: public class FileUriMapper : UriMapperBase
2: {
3: private string gTempUri = string.Empty;
4:
5: public override Uri MapUri(Uri uri)
6: {
7: gTempUri = uri.ToString();
8:
9: // 判斷URI是否為File association type,如果是會有固定的term:/FileTypeAssociation?
10: if (gTempUri.Contains("/FileTypeAssociation") == true)
11: {
12: // 為File assocation type
13: // 取得fileToken=%s中的%s值,透過IndexOf跳過fileToken=的字段
14: string tFileID = gTempUri.Substring(gTempUri.IndexOf("fileToken=") + 10);
15:
16: // 轉向指定處理File Association type的Page
17: return new Uri("/RoutePage.xaml?fileToken=" + tFileID, UriKind.Relative);
18: }
19:
20: // 如果不是File Association type直接回傳,不影響其他URI運作;
21: return uri;
22: }
23: }
上述中可發現/FileTypeAssociation後面有fileToken字段,完整的URI如下:「/FileTypeAssociation?fileToken=
89819279-4fe0-4531-9f57-d633f0949a19」,其中的fileToken即來自WMAppManifest.xml中定義的NavUriFragment。
(B-2). 將自訂的URIMapperBase取代應用程式中預設的URIMapper模組:
為了處理File Association上述自訂了URIMapper,接下來需至App.xaml.cs中取代掉應用程式預設的URIMapper。
此處需要注意,要修改在InitializePhoneApplication這個方法裡。如下程式:
1: // Do not add any additional code to this method
2: private void InitializePhoneApplication()
3: {
4: if (phoneApplicationInitialized)
5: return;
6:
7: // Create the frame but don't set it as RootVisual yet; this allows the splash
8: // screen to remain active until the application is ready to render.
9: RootFrame = new PhoneApplicationFrame();
10: RootFrame.Navigated += CompleteInitializePhoneApplication;
11:
12: // 將自訂的URIMapper取代預設的UriMapper
13: RootFrame.UriMapper = new FileUriMapper();
14:
15: // Handle navigation failures
16: RootFrame.NavigationFailed += RootFrame_NavigationFailed;
17:
18: // Handle reset requests for clearing the backstack
19: RootFrame.Navigated += CheckForResetNavigation;
20:
21: // Ensure we don't initialize again
22: phoneApplicationInitialized = true;
23: }
C. 實作轉向的Page,負責讀取與呈現檔案的內容:
以下為在OnNavigatedTo根據NavigationContext.QueryString的內容,轉向處理File Association或讀取External Storage中的資料;
1: protected override void OnNavigatedTo(NavigationEventArgs e)
2: {
3: base.OnNavigatedTo(e);
4:
5: string tFilePath = string.Empty;
6:
7: // 負責取得URI帶入的fileToke參數後,進行檔案的讀取與呈現;
8: if (NavigationContext.QueryString.ContainsKey("fileToken") == true)
9: {
10: // 負責處理File Association的邏輯
11: tFilePath = NavigationContext.QueryString["fileToken"];
12: ProcessExternalFile(tFilePath);
13: }
14: else if (NavigationContext.QueryString.ContainsKey("sdFilePath") == true)
15: {
16: // 負責讀取External Storage中的內容,透過SdFilePath來讀取檔案;
17: tFilePath = NavigationContext.QueryString["sdFilePath"];
18: ProcessSDGPXFile(tFilePath);
19: }
20: }
(C-1). 讀取Local Folder下的檔案;(負責處理一般File Associations的邏輯):
1: /// <summary>
2: /// 負責處理file association中指定的檔案。
3: /// </summary>
4: /// <param name="tFileToken"></param>
5: private async void ProcessExternalFile(string tFileToken)
6: {
7: // 建立或打開指定目錄: data
8: IStorageFolder tFolder = await Windows.Storage.ApplicationData.Current.LocalFolder
9: .CreateFolderAsync("data", CreationCollisionOption.OpenIfExists);
10: // 透過GetSharedFileName取得由File Association中帶入的指定File name
11: string tincomingRouteFilename = Windows.Phone.Storage.SharedAccess.SharedStorageAccessManager
12: .GetSharedFileName(tFileToken);
13:
14: // 複製指定的檔案至指定的目錄
15: IStorageFile tFile = await Windows.Phone.Storage.SharedAccess.SharedStorageAccessManager
16: .CopySharedFileAsync(
17: (StorageFolder)tFolder,
18: tincomingRouteFilename,
19: NameCollisionOption.ReplaceExisting,
20: tFileToken);
21: // 建立一個Stream取得檔案
22: var tStream = await tFile.OpenReadAsync();
23:
24: // 讀取檔案
25: ReadFile(tStream);
26: }
此段程式主要將File Association帶入的Share File先寫入Local Folder下,再讀取Local Folder中的檔案進行存取;
(C-2). 讀取SD Card下的檔案;
1: /// 負責處理External Storage中檔案的讀取。
2: /// </summary>
3: /// <param name="pSDFilePath">SD File Path</param>
4: /// <returns></returns>
5: private async Task ProcessSDGPXFile(string pSDFilePath)
6: {
7: // 取得目前可用的ExternalStorageDevice,手機只有一個,所以FirstOrDefault即可
8: ExternalStorageDevice tSdCard = (await ExternalStorage.GetExternalStorageDevicesAsync())
9: .FirstOrDefault();
10: if (tSdCard != null)
11: {
12: try
13: {
14: // 從SD Card根據File Path取得檔案物件.
15: ExternalStorageFile file = await tSdCard.GetFileAsync(pSDFilePath);
16:
17: // 讀取檔案為Stream.
18: Stream s = await file.OpenForReadAsync();
19:
20: // 呈現檔案內容.
21: ReadFile(s);
22: }
23: catch (FileNotFoundException)
24: {
25: // The route is not present on the SD card.
26: MessageBox.Show("That route is missing on your SD card.");
27: }
28: }
29: else
30: {
31: // No SD card is present.
32: MessageBox.Show("The SD card is mssing. Insert an SD card that has a " +
33: "Routes folder containing at least one .GPX file and try again.");
34: }
35: }
D. 實作讀取指定ExternalStorage檔案的邏輯:
上述(C-2).看到透過SD File Path進行讀取檔案的方式,該段顯示如何取得External Storage Folder下的所有檔案;
1: // 掃瞄SD Card中任何為GPX副檔名的檔案。
2: private async void scanExternalStorageButton_Click_1(object sender, RoutedEventArgs e)
3: {
4: // 先行清除放置ExternalStorageFile的集合。
5: Routes.Clear();
6:
7: // 連結至目前可用的SD Card。
8: ExternalStorageDevice _sdCard = (await ExternalStorage.GetExternalStorageDevicesAsync()).FirstOrDefault();
9:
10: // 如果SD Card可以被讀取,增加SD Card中所有的*.gpx檔案至集合。
11: if (_sdCard != null)
12: {
13: try
14: {
15: // 取得SD Card中的指定目錄。
16: ExternalStorageFolder routesFolder = await _sdCard.GetFolderAsync("Routes");
17:
18: // 取得該目錄下的所有檔案。
19: IEnumerable<ExternalStorageFile> routeFiles = await routesFolder.GetFilesAsync();
20:
21: // 將*.gpx加入至集合中
22: foreach (ExternalStorageFile esf in routeFiles)
23: {
24: // 判斷Path屬性值是否結束為.gpx
25: if (esf.Path.EndsWith(".gpx"))
26: {
27: Routes.Add(esf);
28: }
29: }
30: }
31: catch (FileNotFoundException)
32: {
33: // No Routes folder is present.
34: MessageBox.Show("The Routes folder is missing on your SD card. Add a Routes folder containing at least one .GPX file and try again.");
35: }
36: }
37: else
38: {
39: // No SD card is present.
40: MessageBox.Show("The SD card is mssing. Insert an SD card that has a Routes folder containing at least one .GPX file and try again.");
41: }
42: }
上述程式擷錄<Route mapper sample>,其作法非常容易,但重點在於External Storage File的讀取,需透過Path屬性值。
[範例程式]
範例程式,主要參考<Route mapper sample>的內容。但要注意Emulator不支援SD Card(External Storage)的模擬。
了解了怎麼撰寫一個範例之後,接下來討論在上述範例提到的幾個重要的類別。
提供Read-only連結手機SD Card的方式。主要使用GetExternalStorageDevicesAsync()方法回傳IEnumerable<ExternalStorageDevice>
集合,但目前只有一個SD Card的支援,可以直接取得預設或第一個項目為ExternalStorageDevice物件。
用於代表電話中的SD Card本身。需搭配ExternalStorage.GetExternalStorageDevicesAsync()方法來取得目前可用的SD Card。
取得的SD Card物件是Read-only的,無法進行編輯或創建新的檔案資訊。
有二個可用的屬性:
Name | Description |
ExternalStorageID | 取得SD Card本身的識別ID。 |
RootFolder | 取得SD Card的根目錄,回傳ExternalStorageFolder物件。 |
幾個常用的方法:
Name | Description |
GetFileAsync | 透過指定的Path取得目前SD Card下的單一檔案。 |
GetFolderAsync | 透過指定的Path取得目前SD Card下的單一目錄。 |
代表SD Card中的一個目錄。其重要的屬性與方法如下:
Name | Description |
DateModified | 表示目前目錄最後一次被修改的日期與時間。 |
Name | 取得目錄的名稱。 |
Path | 取得目前目錄的路徑。 |
GetFilesAsync | 取得目前目錄下top-level(第一層)的檔案清單。 |
GetFolderAsync | 使用Path取得目前目錄下的單一子目錄。 |
GetFoldersAsync | 取得目前目錄下top-level(第一層)的目錄清單。 |
代表SD Card中的一個檔案。其重要的屬性與方法如下:
Name | Description |
DateModified | 表示目前檔案最後一次被修改的日期與時間。 |
Name | 取得檔案的名稱,包括副檔名。 |
Path | 取得目前檔案的路徑。 |
Dispose() | 釋放該檔案占用的所有資源。 |
OpenForReadAsync() | 開啟一個Stream來讀取目前檔案的內容。 |
上述介紹主要是Microsoft.Phone.Storage Namespace下主要四個類別,使用的方法可以參考上述的範例,
主要需要先識別有無ExternalStorageDevice,接著根據Path取得需要的目錄與檔案。
另外,補充在應用程式進行File Associations時,必要注意的重要類別:
提供存取在File Association傳遞過程中的檔案。該類別是靜態類別有二個重要的方法,分別如:
Method | Description |
GetSharedFileName | 回傳在File Association中分享的檔案名稱。 由於在File Association中傳遞的是一個GUID,因此,需藉由該方法取得檔案名稱。 |
CopySharedFileAsync | 複製一個檔案至指定的路徑。被複製的檔案將被File Associations分享至其他應用程式。 該方法搭配GetSharedFileName取得檔案名稱,並與GUID一併取得實際的檔案。 |
這二個方法搭配使用的範例,可參考上述的範例程式。
[補充]
以下針對在研究過程裡,比較常被提到與需要了解的類別加以說明:
‧IRandomAccessStreamWithContentType:一種提供Random access讀取input/output stream的機制;
‧CommonFolderQuery:列舉值,可搭配StorageFolder進行GetFoldersAsync的查詢與GroupBy…等功能。
‧CommonFileQuery:列舉值,可搭配StorageFile進行GetFilesAsync的查詢與GroupBy…等功能。
‧DataReader:屬於Windows.Phone.Streams命名空間中的讀取器,該Namespace下還有很多Stream的類別值得注意。
‧OpenStreamForReadAsync:在指定的folder中讀取某一檔案並轉成Stream;需依賴System.IO的引用才可以使用。
‧OpenStreamForWriteAsync: 在指定的folder中透過Stream寫入一檔案;需依賴System.IO的引用才可以使用。
======
以上是介紹有關External Storage API與File Association的使用機制。不過很可惜我手邊沒有WP8實機,不然就可以
Screenshot下執行的畫面了。不過目前External Storage API只有Read-only存取權限,不像Android有寫入跟刪除的權限。
不過我想在安裝程式時,系統支援程式可裝至SD Card,那是否代表Isolated Storage與Local Storage所面對的來源即時SD Card呢?
實際上存取的路徑不是非常了解,也許我得到實機之後可以來了解一下。希望對大家有所幫助。
References:
‧Data for Windows Phone (重要)
‧Reading from the SD card on Windows Phone 8 (重要)
‧How to load file resources (Windows Store apps using JavaScript and HTML)
‧How to: Use the Isolated Storage Explorer Tool
‧Windows Phone 8: Notes from the SDK
‧Windows Phone 8 – Storages的轉變 & Windows Phone 8 - 練習操作Storages
‧How to modify the app manifest file for Windows Phone.
‧Route mapper sample (重要)
‧Quickstart: Data binding to controls for Windows Phone
‧Maps and navigation for Windows Phone 8
‧Auto-launching apps using file and URI associations for Windows
‧App capabilities and hardware requirements for Windows Phone
‧WinRT 中RandomAccessStreamReference的使用