WP8 - 操作External Storage API

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)的模擬

 

 

了解了怎麼撰寫一個範例之後,接下來討論在上述範例提到的幾個重要的類別。

ExternalStorage

     提供Read-only連結手機SD Card的方式。主要使用GetExternalStorageDevicesAsync()方法回傳IEnumerable<ExternalStorageDevice>

集合,但目前只有一個SD Card的支援,可以直接取得預設或第一個項目為ExternalStorageDevice物件。

 

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下的單一目錄。

 

ExternalStorageFolder

     代表SD Card中的一個目錄。其重要的屬性與方法如下:

Name Description
DateModified 表示目前目錄最後一次被修改的日期與時間。
Name 取得目錄的名稱。
Path 取得目前目錄的路徑。
GetFilesAsync 取得目前目錄下top-level(第一層)的檔案清單。
GetFolderAsync 使用Path取得目前目錄下的單一子目錄。
GetFoldersAsync 取得目前目錄下top-level(第一層)的目錄清單。

 

ExternalStorageFile

     代表SD Card中的一個檔案。其重要的屬性與方法如下:

Name Description
DateModified 表示目前檔案最後一次被修改的日期與時間。
Name 取得檔案的名稱,包括副檔名。
Path 取得目前檔案的路徑。
Dispose() 釋放該檔案占用的所有資源。
OpenForReadAsync() 開啟一個Stream來讀取目前檔案的內容。

 

上述介紹主要是Microsoft.Phone.Storage Namespace下主要四個類別,使用的方法可以參考上述的範例,

主要需要先識別有無ExternalStorageDevice,接著根據Path取得需要的目錄與檔案。

 

另外,補充在應用程式進行File Associations時,必要注意的重要類別:

SharedStorageAccessManager

     提供存取在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的使用

 

Dotblogs Tags: