行動裝置的資料離線解決方案Part 2

by Anita 5. 十一月 2014 05:01

 

.NET Magazine國際中文電子雜誌
者:羅慧真
稿:張智凱
文章編號:N141115401
出刊日期:2014/11/05
開發工具:Visual Studio 2013
版本:Windows Runtime 1.1

在 Part 1 我們完成了Web API 行動服務及資料表已經建置完成第一階段的任務。這一篇我們要來談談使用 Windows Phone Store App 與 NoSQL 的應用。

NoSQL 是甚麼?

基本上是沒有資料庫引擎,將資料存於記憶體或是檔案中,並且以物件集合的形式存在。也就是你得要使用WinRT的HttpClient、File、Stream、LINQ 等類別庫處理應用程式所需的資料,可以用在Windows Store App及Windows Phone Store App。

  • HttpClient 類別,是用來與遠端服務溝通的API,使用HTTP 網路協定。下載下來的資訊通常是XML 或是 JSON 格式的內容,必須經過序列化程序重新載入記憶體,或是將記憶體物件反序列化變成XML 或是 JSON 格式的內容上傳到網路服務端。
  • DataContractJsonSerializer 類別,提供JSON 序列化 / 反序列化的功能。
  • File / Stream 相關類別,提供將資料存入檔案系統,或從檔案系統讀出資料的功能。
  • LINQ 技術,提供物件集合的查詢功能。

image

這裡我們使用Windows Phone Store App作為我們的用戶端,首先建立一個Windows Phone 應用程式。開啟 Visual Studio 2013 ,在選單中選取【檔案】 > 【新增】 > 【專案】,出現以下畫面,請在左邊視窗選取【Visual C#】 > 【Windows Phone】,右邊窗格選取【Windows Phone應用程式】。

image

在 Part1 中我們曾在Web API的專案中建立一個Student的類別及資料庫:

public class Student{
   public string Id { get; set; }
   public string FirstName { get; set; }
   public string LastName { get; set; }
   [Microsoft.WindowsAzure.MobileServices.Version]
   public string Version { get; set; }
}

用戶端這裡必須有一個物件類別結構是與服務端相稱的。請在【方案總管】加入一個資料夾取名為【DataModel】,在 DataModel 資料夾中加入一個類別取名為:Student.cs,加入程式碼如下:

public class Student {
    public string id { get; set; }
    public string firstName { get; set; }
    public string lastName { get; set; }
}

特別注意的地方是這裡的名稱必須根據 json 封包的格式大小寫來實作,請看下圖,雖然在Service端類別欄位的定義是Pascal 命名法,但是序列化成json之後變成camel 命名法。

image

設計畫面:

開啟MainPage.aspx,預備畫面如下:

image

四個Button,一個ListBox,ListBox的ItemTemplate 使用StackPanel 放置兩個 TextBlock 控制項,並且與資料來源的firstName、lastName 做資料繫結。本文不在贅述DataTemplate的作法,請自行參考本網站的相關文章。完成後 XAML 內容如下:

<Page.Resources>
   <DataTemplate x:Key="DataTemplate1">
     <StackPanel Orientation="Horizontal" >
       <TextBlock Text="{Binding firstName}" FontSize="32" Foreground="Blue" />
      <TextBlock Text="{Binding lastName}" FontSize="24"/>
    </StackPanel>
</DataTemplate>
</Page.Resources>
<Grid>
   <ListBox x:Name="listBox1" Margin="32,126,27,24" ItemTemplate="{StaticResource DataTemplate1}"/>
   <Button x:Name="downloadButton" Content="下載" HorizontalAlignment="Left" Height="24" Margin="32,2,0,0" VerticalAlignment="Top" Width="69" Click="downloadButton_Click"/>
   <Button x:Name="saveButton" Content="存到本地" HorizontalAlignment="Left" Height="23" Margin="32,63,0,0" VerticalAlignment="Top" Width="53" Click="saveButton_Click"/>
   <Button x:Name="loadButton" Content="本地載入" HorizontalAlignment="Left" Height="23" Margin="157,63,0,0" VerticalAlignment="Top" Width="162" Click="loadButton_Click"/>
   <Button x:Name="uploadButton" Content="新增 > 雲端" Height="60" Width="162" Margin="157,0,0,580" Click="uploadButton_Click"/>
</Grid>

從Web API下載資料

開啟MainPage.xaml.cs檔,在MainPage 類別中宣告以下兩個變數。baseAddress是 Web API 的網址,studentsNoSQL 是一個 List 泛型集合,型別是Student,預備用來取得Web API 的資料之後暫存的變數。

string baseAddress = "http://localhost:59220/";

List<Student> studentsNoSQL = new List<Student>();

接著在MainPage類別加入一個方法 -- downloadStudents:

private async Task downloadStudents(){
   HttpClientHandler handler = new HttpClientHandler();
   if (handler.SupportsAutomaticDecompression)   {
      handler.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
   }
   HttpClient client = new HttpClient(handler);
   var response = await client.GetAsync(baseAddress + "tables/Student");
   if (response.IsSuccessStatusCode)   {
      var stream = await response.Content.ReadAsStreamAsync();
      var ser = new DataContractJsonSerializer(typeof(List<Student>));
      studentsNoSQL = ser.ReadObject(stream) as List<Student>;
   }
   listBox1.ItemsSource = studentsNoSQL;
}

  • HttpClientHandler

   HttpClientHandler handler = new HttpClientHandler();
   if (handler.SupportsAutomaticDecompression)

HttpClientHandler用來處理HttpClient 網路協定溝通時的設定,像是網路傳遞時是否使用壓縮功能。程式中使用SupportsAutomaticDecompression屬性判斷是否支援自動壓縮功能,若有設定使用Gzip 或Deflate 壓縮的演算法。

  • HttpClient建構函式

HttpClient client = new HttpClient(handler);

傳入HttpClientHandler參數代表根據HttpClientHandler的設定處理網路通訊。

  • GetAsync

var response = await client.GetAsync(baseAddress + "tables/Student");
if (response.IsSuccessStatusCode)

GetAsync是HttpClient的方法成員,傳入要連結的網址,以非同步的方式存取。回傳HttpResponseMessage物件,它的IsSuccessStatusCode屬性是用來判斷回應的狀態碼是否成功。

  • ReadAsStreamAsync

var stream = await response.Content.ReadAsStreamAsync();

ReadAsStreamAsync 是HttpResponseMessage的方法成員,用來以非同步的方式讀取回應的串流,他會回傳一個System.IO.Stream 的物件。

  • ReadObject

var stream = await response.Content.ReadAsStreamAsync();
var ser = new DataContractJsonSerializer(typeof(List<Student>));
studentsNoSQL = ser.ReadObject(stream) as List<Student>;

ReadObject是DataContractJsonSerializer的方法成員,將串流傳遞進去,它會將JSON 格式的串流反序列化為物件實體,回傳的型別是 Object ,故須轉型為List<Student>。

  • 資料繫結

listBox1.ItemsSource = studentsNoSQL;

最後將得到的studentsNoSQL 的變數與ListBox1 控制項資料繫結〔ItemsSource屬性〕。

接著在downloadButton的 Click事件呼叫downloadStudents,使用await 呼叫此方法,並在事件處理程序上加上 async 關鍵字。

private async void downloadButton_Click(object sender, RoutedEventArgs e){
    await downloadStudents();
}

按F5執行,會開啟Windows Phone 模擬器,在畫面中按【下載】按鈕。下圖是以除錯模式逐行除錯方式取得的畫面,我們成功的將Web API的 Student 資料下載並且還原序列成為studentsNoSQL集合物件了。

image

執行結果如下:

image

結束程式,回到設計狀態。

資料存檔

在saveButton的Click事件中寫入以下程式碼,以將資料儲存到本地的檔案系統,程式碼如下:

private async void saveButton_Click(object sender, RoutedEventArgs e)
{
   var file = await ApplicationData.Current.LocalFolder.CreateFileAsync("Student.dat", CreationCollisionOption.ReplaceExisting);
   var outStream = await file.OpenStreamForWriteAsync();
   var ser = new DataContractJsonSerializer(typeof(List<Student>));
   ser.WriteObject(outStream, studentsNoSQL);
   await outStream.FlushAsync();
   outStream.Dispose();
}

  • LocalFolder

var file = await ApplicationData.Current.LocalFolder.CreateFileAsync("Student.dat", CreationCollisionOption.ReplaceExisting);

LocalFolder 是ApplicationData.Current.LocalFolder,取得這個應用程式專用的本地儲存目錄,回傳StorageFolder 物件。

CreateFileAsync是非同步方法,這個應用程式專用的本地儲存目錄中建立一個檔案,回傳StorageFile 物件。

CreationCollisionOption.ReplaceExisting,所建立的檔案若是存在就以新的內容取代。

  • OpenStreamForWriteAsync

var outStream = await file.OpenStreamForWriteAsync();

OpenStreamForWriteAsync是StorageFile 物件的非同步方法,開啟可寫入的資料串流,回傳Stream物件。

  • WriteObject

var ser = new DataContractJsonSerializer(typeof(List<Student>));

ser.WriteObject(outStream, studentsNoSQL);

WriteObject 是DataContractJsonSerializer 物件提供的方法。第一個參數是要輸出的Stream物件,第二個參數是要被序列化成為JSON格式的物件。

  • FlushAsync

await outStream.FlushAsync();

FlushAsync 是Stream物件提供的方法。將暫存區的資料清出,以確保資料都已寫到Stream 物件所連接的檔案。

下面是執行【存到本地】的逐步除錯過程,這行正在將studentsNoSQL序列化成為JSON串流寫到StorageFile 的檔案中。

image

從檔案載入資料

在loadButton的Click事件中寫入以下程式碼,以將從本地的檔案系統載入並還原序列回List<Student>物件,程式碼如下:

private async void loadButton_Click(object sender, RoutedEventArgs e) {

   var file = await ApplicationData.Current.LocalFolder.GetFileAsync("Student.dat");
   var inStream = await file.OpenStreamForReadAsync();
   var ser = new DataContractJsonSerializer(typeof(List<Student>));
   studentsNoSQL = ser.ReadObject(inStream) as List<Student>;
   listBox1.ItemsSource = studentsNoSQL;
   inStream.Dispose();

}

  • GetFileAsync

var file = await ApplicationData.Current.LocalFolder.GetFileAsync("Student.dat");

GetFileAsync 是ApplicationData.Current.LocalFolder的非同步方法,用來取得放置在LocalFolder中的指定檔名,回傳StorageFile物件。

  • OpenStreamForReadAsync

var inStream = await file.OpenStreamForReadAsync();

OpenStreamForReadAsync 是StorageFile物件的非同步方法,開啟StorageFile中的串流以讀取當中的序列串流。

  • ReadObject

var ser = new DataContractJsonSerializer(typeof(List<Student>));
studentsNoSQL = ser.ReadObject(inStream) as List<Student>;
listBox1.ItemsSource = studentsNoSQL;

ReadObject是DataContractJsonSerializer的方法成員,將檔案串流傳遞進去,它會將JSON 格式的串流反序列化為物件實體,回傳的型別是 Object ,故須轉型為List<Student>。

下圖是以除錯模式逐行除錯方式取得的畫面,透過 GetFileAsync 方法將先前存在應用程式專用的本機資料夾中的檔案載入。從中段點中的 file 變數中可以查看 Path 屬性得知其確切的位置。

image

上傳資料到Web API

在說明上傳資料到Web API 時,我們先預備幾個方法,第一個是JSONSerializer泛型方法,這個方法要用來將傳入的物件轉換為JSON字串。因為DataContractJsonSerializer所提供的WriteObject 方法目前只能將物件序列化為 Stream 物件,但是在傳遞 Web API 時我們預備要傳的是JSON String。

string JSONSerializer<T>( T obj) {

   var ser = new DataContractJsonSerializer(typeof(T));
   var stream = new MemoryStream();
   ser.WriteObject(stream, obj);
   stream.Flush();
   stream.Position = 0;
   var reader = new StreamReader(stream);
   return reader.ReadToEnd();

}

  • String JSONSerializer<T> (T Obj)

string JSONSerializer<T>( T obj){

使用 <T> 宣告泛型方法,呼叫時指定要傳入的Obj 是何種型別,編譯器便會以該種型別處理程序。

  • MemoryStream

var stream = new MemoryStream();
ser.WriteObject(stream, obj);

存放在記憶體的資料流。這裡預備將傳入的 obj 物件序列化後儲存在記憶體的資料流當中。

WriteObject,DataContractJsonSerializer的方法,將資料以 JSON 格式序列化寫入到 MemoryStream 當中。

  • Flush

stream.Flush();
stream.Position = 0;

Flush是MemoryStream 的方法。確保在暫存區的資料完全被清出並寫入MemoryStream。

Position,MemoryStream 的屬性。當資料寫入後 MemoryStream 的Position 會移到最後一個位置。如果想讀取內容的話必須將位置指標一到開頭的位置。

  • StreamReader

var reader = new StreamReader(stream);
return reader.ReadToEnd();

StreamReader類別會將Byte Array 的串流讀出成為字串型別的資訊。

ReadToEnd,將Stream 內容以字串格式讀出,並傳回String 型別的資料。這段程式的結果會是 JSON 字串。

在MainPage 類別加入ShowMessage 方法,此方法提供訊息方塊的功能。

private static async Task ShowMessage(string message) {

   MessageDialog msg = new MessageDialog(message);
   await msg.ShowAsync();

}

  • MessageDialog,提供訊息方塊的類別,傳入string 型別的參數是要顯示的訊息字串。
  • ShowAsync,MessageDialog 的非同步方法,這個方法會顯示訊息在訊息方塊中。

在 uploadButton 的 Click 事件中建立一個新的 Student 物件,並且透過 HttpClient 將新建立的 Student 物件上傳到 Web API。

private async void uploadButton_Click(object sender, RoutedEventArgs e) {

   HttpClientHandler handler = new HttpClientHandler();
   if (handler.SupportsAutomaticDecompression)   {
      handler.AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate;
   }

   HttpClient client = new HttpClient(handler);
   Student newStudent = new Student (){firstName = "保羅", lastName="江"};
   string data = JSONSerializer<Student>(newStudent);
   HttpContent content = new StringContent(data);
   content.Headers.ContentType= new System.Net.Http.Headers.MediaTypeHeaderValue( "application/json");
   var response = await client.PostAsync (baseAddress + "tables/Student", content);
   if (response.IsSuccessStatusCode) {
      await downloadStudents();
   }
   else { 
      string error = "紀錄無法新增成功";
      await ShowMessage(error);
   }
}

  • new Student

Student newStudent = new Student (){firstName = "保羅", lastName="江"};

建立一個 Student 的物件

  • JSONSerializer<Student>(newStudent)

string data = JSONSerializer<Student>(newStudent);

使用JSONSerializer將它序列化成為 JSON 字串。

  • HttpContent

HttpContent content = new StringContent(data);
content.Headers.ContentType= new System.Net.Http.Headers.MediaTypeHeaderValue( "application/json");

使用StringContent 將序列化後的 Student JSON 字串轉化成為 Http Body 的內容,並定義 HttpContent 的 Header 的內容格是為 "application/json" 。

  • PostAsync

var response = await client.PostAsync (baseAddress + "tables/Student", content);

PostAsync 是 HttpClient 的方法,意指使用 POST 的方式傳遞封包,第一個參數是Web API 的網址,第二個參數是要傳遞的內容。

  • response

if (response.IsSuccessStatusCode) {
   await downloadStudents();
}

如果Web API 回應成功就重新下載最新的清單。

下圖是程式執行到JSONSerializer<Student> 時,你可以看到它序列化成為字串的樣子。

image

執行完成之後,你會看到清單中多了一筆資料。

image

Windows Phone Store App 的程式使用 POSTAsync 呼叫時:

var response = await client.PostAsync (baseAddress + "tables/Student", content);

WebAPI 會執行StudentsController 的 PostStudent 方法,這裡會將資料新增的Student 寫到資料庫中。

image

在資料庫中你可以看到新增的紀錄:

image

這一篇我們使用Windows Phone Store App實作 NoSQL 的應用,說穿了就是使用 WinRT 內建的 HttpClient 、 File 、 Stream 等API功能處理下載或是上傳,存檔在本地供離線時使用。在下一篇我將說明如何使用SQLite 與 Windows Store App 時做的技術。

新增評論




  Country flag
biuquote
  • 評論
  • 線上預覽
Loading






NET Magazine國際中文電子雜誌

NET Magazine國際中文電子版雜誌,由恆逸資訊創立於2000,自發刊日起迄今已發行超過500篇.NET相關技術文章,擁有超過40000名註冊讀者群。NET Magazine國際中文電子版雜誌希望藉於電子雜誌與NET Developer達到共同學習與技術新知分享,歡迎每一位對.NET 技術有興趣的朋友們多多支持本雜誌,讓作者群們可以有持續性的動力繼續爬文。<請加入免費訂閱>

月分類Month List