.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 技術,提供物件集合的查詢功能。
這裡我們使用Windows Phone Store App作為我們的用戶端,首先建立一個Windows Phone 應用程式。開啟 Visual Studio 2013 ,在選單中選取【檔案】 > 【新增】 > 【專案】,出現以下畫面,請在左邊視窗選取【Visual C#】 > 【Windows Phone】,右邊窗格選取【Windows Phone應用程式】。
在 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 命名法。
設計畫面:
開啟MainPage.aspx,預備畫面如下:
四個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 handler = new HttpClientHandler();
if (handler.SupportsAutomaticDecompression)
HttpClientHandler用來處理HttpClient 網路協定溝通時的設定,像是網路傳遞時是否使用壓縮功能。程式中使用SupportsAutomaticDecompression屬性判斷是否支援自動壓縮功能,若有設定使用Gzip 或Deflate 壓縮的演算法。
HttpClient client = new HttpClient(handler);
傳入HttpClientHandler參數代表根據HttpClientHandler的設定處理網路通訊。
var response = await client.GetAsync(baseAddress + "tables/Student");
if (response.IsSuccessStatusCode)
GetAsync是HttpClient的方法成員,傳入要連結的網址,以非同步的方式存取。回傳HttpResponseMessage物件,它的IsSuccessStatusCode屬性是用來判斷回應的狀態碼是否成功。
var stream = await response.Content.ReadAsStreamAsync();
ReadAsStreamAsync 是HttpResponseMessage的方法成員,用來以非同步的方式讀取回應的串流,他會回傳一個System.IO.Stream 的物件。
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集合物件了。
執行結果如下:
結束程式,回到設計狀態。
資料存檔
在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();
}
var file = await ApplicationData.Current.LocalFolder.CreateFileAsync("Student.dat", CreationCollisionOption.ReplaceExisting);
LocalFolder 是ApplicationData.Current.LocalFolder,取得這個應用程式專用的本地儲存目錄,回傳StorageFolder 物件。
CreateFileAsync是非同步方法,這個應用程式專用的本地儲存目錄中建立一個檔案,回傳StorageFile 物件。
CreationCollisionOption.ReplaceExisting,所建立的檔案若是存在就以新的內容取代。
var outStream = await file.OpenStreamForWriteAsync();
OpenStreamForWriteAsync是StorageFile 物件的非同步方法,開啟可寫入的資料串流,回傳Stream物件。
var ser = new DataContractJsonSerializer(typeof(List<Student>));
ser.WriteObject(outStream, studentsNoSQL);
WriteObject 是DataContractJsonSerializer 物件提供的方法。第一個參數是要輸出的Stream物件,第二個參數是要被序列化成為JSON格式的物件。
await outStream.FlushAsync();
FlushAsync 是Stream物件提供的方法。將暫存區的資料清出,以確保資料都已寫到Stream 物件所連接的檔案。
下面是執行【存到本地】的逐步除錯過程,這行正在將studentsNoSQL序列化成為JSON串流寫到StorageFile 的檔案中。
從檔案載入資料
在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();
}
var file = await ApplicationData.Current.LocalFolder.GetFileAsync("Student.dat");
GetFileAsync 是ApplicationData.Current.LocalFolder的非同步方法,用來取得放置在LocalFolder中的指定檔名,回傳StorageFile物件。
var inStream = await file.OpenStreamForReadAsync();
OpenStreamForReadAsync 是StorageFile物件的非同步方法,開啟StorageFile中的串流以讀取當中的序列串流。
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 屬性得知其確切的位置。
上傳資料到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 是何種型別,編譯器便會以該種型別處理程序。
var stream = new MemoryStream();
ser.WriteObject(stream, obj);
存放在記憶體的資料流。這裡預備將傳入的 obj 物件序列化後儲存在記憶體的資料流當中。
WriteObject,DataContractJsonSerializer的方法,將資料以 JSON 格式序列化寫入到 MemoryStream 當中。
stream.Flush();
stream.Position = 0;
Flush是MemoryStream 的方法。確保在暫存區的資料完全被清出並寫入MemoryStream。
Position,MemoryStream 的屬性。當資料寫入後 MemoryStream 的Position 會移到最後一個位置。如果想讀取內容的話必須將位置指標一到開頭的位置。
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);
}
}
Student newStudent = new Student (){firstName = "保羅", lastName="江"};
建立一個 Student 的物件
- JSONSerializer<Student>(newStudent)
string data = JSONSerializer<Student>(newStudent);
使用JSONSerializer將它序列化成為 JSON 字串。
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" 。
var response = await client.PostAsync (baseAddress + "tables/Student", content);
PostAsync 是 HttpClient 的方法,意指使用 POST 的方式傳遞封包,第一個參數是Web API 的網址,第二個參數是要傳遞的內容。
if (response.IsSuccessStatusCode) {
await downloadStudents();
}
如果Web API 回應成功就重新下載最新的清單。
下圖是程式執行到JSONSerializer<Student> 時,你可以看到它序列化成為字串的樣子。
執行完成之後,你會看到清單中多了一筆資料。
Windows Phone Store App 的程式使用 POSTAsync 呼叫時:
var response = await client.PostAsync (baseAddress + "tables/Student", content);
WebAPI 會執行StudentsController 的 PostStudent 方法,這裡會將資料新增的Student 寫到資料庫中。
在資料庫中你可以看到新增的紀錄:
這一篇我們使用Windows Phone Store App實作 NoSQL 的應用,說穿了就是使用 WinRT 內建的 HttpClient 、 File 、 Stream 等API功能處理下載或是上傳,存檔在本地供離線時使用。在下一篇我將說明如何使用SQLite 與 Windows Store App 時做的技術。