.NET Magazine國際中文電子雜誌
作 者:羅慧真
審 稿:張智凱
文章編號:N141215502
出刊日期:2014/12/15
開發工具:Visual Studio 2013
版本:WinRT 1.1
在 Part 2 我們完成了Windows Phone Store App 與 NoSQL 的應用。這次我們來看看如何使用 SQLite 解決離線存取以及同步處理的問題。
甚麼是SQLite?
這是一個目前最流行的行動資料庫,它支援多種平台,像是iOS, Android, Python, Mono, Windows 等…。是一種內嵌式、in-process的檔案行資料庫引擎。同時具備小型,快速,可靠,與公共領域支援交易處理 ACID等特色。
要如何取得?
首先連結到SQLite.org 網站下載,進入網站之後點選 download 進入下載頁面,向下捲找到Precompiled Binaries for Windows Runtime,點選 sqlite-winrt81-xxxxxx.vsix。完成下載完成後將.zip 副檔名改成 。
圖 1
安裝及使用 SQLite 在 App 的開發環境
解壓縮剛剛下載好的檔案,會看到附檔名是 .vsix 檔案,雙擊兩下 .vsix 檔案,將出現如下畫面,按下【Install】進行安裝。
圖 2
建立Windows Store App 專案
建立一個新的Windows Store App 專案,左邊選取【Visual C#】>【市集應用程式】>【Windows 應用程式】,右邊選擇【空白應用程式】,指定專案的位置與專案名稱。
圖 3
在方案總管的【參考】項目上按滑鼠右鍵選取【加入參考】,進入【參考管理員】,展開左邊的【Windows 8.1】>【擴充功能】,勾選【Microsoft Visual C++ 2013 Runtime Package for Windows】及【SQLite for Windows Runtime (Windows 8.1)】。
圖 4
注意!! SQLite 不支援Any CPU,必須將專案的平台改成 x86、x64或是 ARM。從【建置】>【組態管理員】開啟組態管理員工具如下圖,便可以設定專案的平台版本。
圖 5
安裝 Azure Mobile Service
資料來源是從雲端下載及同步的,所以這裡需要安裝 Azure Mobile Service。從選單【專案】>【管理NuGet套件】,在視窗的左邊窗格選取【線上】,接著在右上的搜尋方塊輸入【Azure Mobile Services】,會找到【Windows Azure Mobile Services】,然後按下【安裝】。
圖 6
重複這個步驟分別搜尋【SQLiteStore】與【SQLitePCL 】並且安裝它們。選項的完整名稱如下圖:
圖 7
圖 8
安裝完成之後會在【方案總管】的【參考】項目看到新增的以下項目:
圖 9
定義相容於資料庫及資料表
不需要使用任何工具建立資料庫或是使用SQL DDL指令,而是使用類別定義資料的結構,這個結構類似於行動服務端專案所建立的類別結構。在專案目錄中建立一個DataModel的資料夾,接著在當中加入一個類別取名為Student,程式碼如下:
using Microsoft.WindowsAzure.Mobile.Service;
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; }
}
使用Client 端連結到行動服務
開啟 App.xaml.cs 檔案,在這個檔案中加入命名空間的參考:
using Microsoft.WindowsAzure.MobileServices;
接著在 App 類別宣告 MobileService 變數,如下:
sealed partial class App : Application
{
public static MobileServiceClient MobileService = new MobileServiceClient("http://localhost:59220");
// Use this constructor instead after publishing to the cloud
// public static MobileServiceClient MobileService = new MobileServiceClient(
// "https://win309.azure-mobile.net/",
// "TLmnlofUjXneKtltNHpuwYJMrfSRKG24" //);
這段程式請參考Microsoft Azure你的帳號中所建立的行動服務中的 【2 連接你的應用程式並將資料儲存在你的服務中您的應用程式並將資料儲存在您的服務中您的應用程式並將資料儲存在您的服務中您的應用程式並將資料儲存在您的服務中】。
圖 10
加入一個類別取名為SyncHandler。在類別中參考以下命名空間:
using Demo.DataModel;
using Microsoft.WindowsAzure.MobileServices;
using Microsoft.WindowsAzure.MobileServices.Sync;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Popups;
這個類別實作IMobileServiceSyncHandler 介面,程式碼如下:
public class SyncHandler : IMobileServiceSyncHandler
{
private MobileServiceClient mClient;
const string LOCAL_VERSION = "使用本地端的版本";
const string SERVER_VERSION = "使用伺服器端的版本";
public SyncHandler(MobileServiceClient client)
{
mClient = client;
App.StudentTable = client.GetSyncTable<Student>();
}
public async System.Threading.Tasks.Task<Newtonsoft.Json.Linq.JObject> ExecuteTableOperationAsync(IMobileServiceTableOperation operation)
{
MobileServicePreconditionFailedException error;
do {
error = null;
try {
return await operation.ExecuteAsync();
}
catch (MobileServicePreconditionFailedException ex) {
// pick up the server-side value from the exception.
error = ex;
}
//自訂這裡做衝突處理
if (error != null) {
var serverValue = error.Value;
var dialog = new MessageDialog("你想要如何解決資料差異的衝突?", "請選擇 本地 / 伺服器端 / 取消");
dialog.Commands.Add(new UICommand(LOCAL_VERSION));
dialog.Commands.Add(new UICommand(SERVER_VERSION));
dialog.Commands.Add(new UICommand("取消"));
IUICommand command = await dialog.ShowAsync();
if (command.Label == LOCAL_VERSION){
operation.Item[MobileServiceSystemColumns.Version ] = serverValue[MobileServiceSystemColumns.Version];
continue;}
else if (command.Label == SERVER_VERSION){
return (JObject)serverValue; }
else{
operation.AbortPush(); }
}
} while (error != null);
return null;
}
public System.Threading.Tasks.Task OnPushCompleteAsync(MobileServicePushCompletionResult result)
{
return (Task.FromResult(0));
}
}
在 App 類別加入一個方法取名為CreateDatabase,作為初始化本地端資料庫的程序。
private async void CreateDatabase() {
if (!client.SyncContext.IsInitialized) {
//Create SQLite Database
var store = new MobileServiceSQLiteStore("mystore.db");
//Create Student Table
store.DefineTable<Student>();
//Initialize Sync Context
await client.SyncContext.InitializeAsync(store, new SyncHandler(client));
}
}
- client.SyncContext.IsInitialized,用來確認資料庫是否已經建立。
- new MobileServiceSQLiteStore("mystore.db"),指定SQLite資料庫的名稱。
- store.DefineTable<Student>();,定義Student 資料表對應的類別結構。
- client.SyncContext.InitializeAsync,開始建立資料庫初始化的程序。
在OnLanuched方法中,在以下程式碼之前:
if (rootFrame.Content == null) {
// When the navigation stack isn't restored navigate to the first page,
// configuring the new page by passing required information as a navigation
// parameter
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
加入呼叫CreateDatabase方法,如下:
CreateDatabase();
if (rootFrame.Content == null)
{
……
按下【F5】執行專案,在啟動程式時會執行CreateDatabase方法,若是尚未建立store.db便會根據程序建立資料庫。可以從檔案總管看到store.db的蹤跡。
圖 11
設計StudentPage.xaml 頁面
回到Visual Studio 2013,在Windows Store App 專案中加入一個空白頁取名為StudentPage.xaml。
在畫面中放入如下幾個Button及 ListBox如下:
圖 12
開啟App.xaml.cs,找到OnLanuched方法中以下這行程式碼:
rootFrame.Navigate(typeof(MainPage), e.Arguments);
將MainPage改成StudentPage:
rootFrame.Navigate(typeof(StudentPage), e.Arguments);
SQLite 的資料新、刪、修處理
不需要使用SQL DML標準的指令或是任何SQL 工具,而是直接利用API 所提供的功能及LINQ 技術進行資料的查詢、新增、刪除、修改等處理作業。
studentList.ItemsSource = await (from item in App.StudentTable
select item).ToListAsync();
- 新增命令:使用InsertAsync方法將新增的Student物件傳入參數即可:
Student st = new Student { FirstName = "雅各",
LastName = "林" };
await App.StudentTable.InsertAsync(st);
- 修改命令:則是使用UpdateAsync將要更新的Student物件傳入參數即可:
var query = (await App.StudentTable.ToListAsync())
.Where(st => st.LastName == "黃");
foreach (var student in query)
{
student.FirstName = "彼得";
await App.StudentTable.UpdateAsync(student);
}
- 刪除命令:使用DeleteAsync方法將要刪除的Student物件傳入參數即可:
var query = from s in App.StudentTable
where s.LastName == “” select s;
foreach (var student in (await query.ToEnumerableAsync()))
{
await App.StudentTable.DeleteAsync(student);
}
開啟StudentPage.xaml.cs,在當中加入LoadData方法,進行查詢資料的作業,並且與studentList物件( ListBox 控制項 )資料繫結,指定ItemsSource屬性。
private async System.Threading.Tasks.Task LoadData()
{
studentList.ItemsSource = await (from item in App.StudentTable
select item).ToListAsync();
}
【載入】按鈕的事件呼叫LoadData,程式如下:
private async void loadButton_Click(object sender, RoutedEventArgs e)
{
await LoadData();
}
在【新增五筆資料】按鈕的事件新增五筆紀錄,程式如下:
private async void instertButton_Click(object sender, RoutedEventArgs e)
{
Student st = new Student { FirstName = "雅各", LastName = "林" };
await App.StudentTable.InsertAsync(st);
await App.StudentTable.InsertAsync(new Student { FirstName = "亞倫", LastName = "陳" });
await App.StudentTable.InsertAsync(new Student { FirstName = "瑪莉", LastName = "王" });
await App.StudentTable.InsertAsync(new Student { FirstName = "西門", LastName = "黃" });
await App.StudentTable.InsertAsync(new Student { FirstName = "猶大", LastName = "" });
}
在【Update (西門 >彼得)】按鈕的事件將FirstName [西門] 改成 [彼得] ,程式如下:
private async void updateButton_Click(object sender, RoutedEventArgs e)
{
// SQLite 無法直接查詢中文字,所以先取回之後再查詢
var query = (await App.StudentTable.ToListAsync()).Where(st => st.LastName == "黃");
foreach (var student in query)
{
student.FirstName = "彼得";
await App.StudentTable.UpdateAsync(student);
}
}
在【Delete (刪除猶大)】按鈕的事件將FirstName為[猶大]的刪除,程式如下:
private async void deleteButton_Click(object sender, RoutedEventArgs e)
{
var query = from s in App.StudentTable where s.LastName == "" select s;
foreach (var student in (await query.ToEnumerableAsync()))
{
await App.StudentTable.DeleteAsync(student);
}
}
按【F5】執行結果,按下【新增五筆資料】按鈕,接著按下【載入資料】按鈕,結果如下:
圖 13
到studentPage.xaml.cs在updateButton_Click事件處理程序的最後一行設定中斷點,然後回到執行畫面,按下【Update (西門 >彼得)】按鈕,可以看到修改過程,如下圖:
圖 14
接著按下【載入資料】按鈕,結果如下:
圖 15
到studentPage.xaml.cs在deleteButton_Click事件處理程序的最後一行設定中斷點,然後回到執行畫面,按下【Delete (刪除猶大)】按鈕,可以看到t刪除過程,如下圖:
圖 16
接著按下【載入資料】按鈕,猶大被刪除了。結果如下:
圖 17
雲端同步與衝突處理
以上的資料是存在本地的磁碟空間。接下來談談如何與雲端的資料同步以及如何處理衝突問題。
藉由雲端的API 進行同步作業,API提供以下功能:
- PUSH:送回雲端資料庫
呼叫 PushAsync() 本地異動的資料送回雲端 SQL資料庫。
- PULL:從雲端擷取資料寫到本地資料表
呼叫 PullAsync() 從雲端取得相應的資料填入本地資料表,必須傳入一個 LINQ 或是 OData 的查詢作為過濾與下載資料的依據。如果本地項目有異動符合查詢的條件,會優先將本地異動的部分同步到雲端,然後執行擷取雲端資料的動作。
- PURGE:清除本地資料
呼叫 PurgeAsync() 清除本地資料表內容。
首先,先回到雲端WEB API 的專案開啟StudentController.cs。當 Client 端的程式進行Push、Pull 作業時,會執行雲端的StudentController API,以下的程式碼以更新內容:
// GET tables/Student
public IQueryable<Student> GetAllStudent()
{
return Query();
}
// GET tables/Student/48D68C86-6EA6-4C25-AA33-223FC9A27959
public SingleResult<Student> GetStudent(string id)
{
return Lookup(id);
}
// PATCH tables/Student/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task<Student> PatchStudent(string id, Delta<Student> patch)
{
return UpdateAsync(id, patch);
}
// POST tables/Student/48D68C86-6EA6-4C25-AA33-223FC9A27959
public async Task<IHttpActionResult> PostStudent(Student item)
{
Student current = await InsertAsync(item);
return CreatedAtRoute("Tables", new { id = current.Id }, current);
}
// DELETE tables/Student/48D68C86-6EA6-4C25-AA33-223FC9A27959
public Task DeleteStudent(string id)
{
return DeleteAsync(id);
}
開啟Windows Store App 專案中的 StudentPage.xaml.cs,在【Push】按鈕的 Click 事件實作以下程式碼,呼叫 PushAsync() 本地異動的資料送回雲端 SQL資料庫,並使用try … catch 處理錯誤:
private async void pushButton_Click(object sender, RoutedEventArgs e)
{
string errorString = string.Empty;
try {
await App.client.SyncContext.PushAsync(new System.Threading.CancellationToken());
}
catch (MobileServicePushFailedException ex){
errorString = "同步發生錯誤 -- Push 失敗: " +
ex.PushResult.Errors.Count() + ", message: " + ex.Message;
}
catch (Exception ex) {
errorString = "Push 失敗: " + ex.Message;
}
if (errorString != string.Empty)
{
MessageDialog md = new MessageDialog(errorString);
await md.ShowAsync();
}
}
在【Pull】按鈕的 Click 事件實作以下程式碼,使用LINQ指令定義要從雲端取回的條件範圍然後呼叫 PullAsync(),並將條件範圍傳入參數中,便會從雲端取得相應的資料填入本地資料表,呼叫更新的LoadData()將資料呈現在畫面上。
private async void pullButton_Click(object sender, RoutedEventArgs e){
Exception pullException = null;
try {
var filter = from c in App.StudentTable select c;
await App.StudentTable.PullAsync<Student>("", filter, new System.Threading.CancellationToken());
await LoadData();
}
catch (Exception ex) {
pullException = ex;
}
if (pullException != null) {
MessageDialog md = new MessageDialog("Pull 失敗: " + pullException.Message);
await md.ShowAsync();
}
}
在【Purge】鈕的 Click 事件實作以下程式碼,使用LINQ指令定義要清除的條件範圍,然後呼叫 PurgeAsync (),並將條件範圍傳入參數中,便會清除本地資料,呼叫更新的LoadData()將資料呈現在畫面上。
private async void purgeButton_Click(object sender, RoutedEventArgs e)
{
string errorString = string.Empty;
try {
var query = from c in App.StudentTable select c;
await App.StudentTable.PurgeAsync("", query, new System.Threading.CancellationToken());
}
catch (Exception ex) {
errorString = "Purge 失敗: " + ex.Message;
}
if (errorString != string.Empty) {
MessageDialog md = new MessageDialog(errorString);
await md.ShowAsync();
}
}
測試時請先將雲端資料表內的資料清除及本地的store.db檔案刪除。執行程式,先按下【新增紀錄】本地資料就會新增,然後,在按下【Push】將資料更新到雲端,此時查看雲端資料庫內容就會有紀錄。下圖是雲端資料庫新增了五筆紀錄。
圖 18
回到應用程式按下【Update】、【Delete】更新及刪除紀錄,接著按下【Pull】,他會先將本地的異動內容寫到雲端,在下載雲端的資料到本地端,結果是雲端與本地的內容會一致。下圖是本地資料在經過修改與刪除之後的結果:
圖 19
下圖是雲端資料表的紀錄,兩邊內容現在是一致的:
圖 20
回到應用程式,按下【Purge】按鈕之後本地的資料全面清除,下圖是清單內的資料目前是被清空的狀態:
圖 21
接著按下【Pull】按鈕,得到的結果如下圖:
圖 22
現在我們刻意製造一個衝突的狀況,在本地端將瑪莉改成瑪麗亞,程式碼如下:
private async void Button_Click(object sender, RoutedEventArgs e)
{
var query = (await App.StudentTable.ToListAsync()).Where(st => st.FirstName == "瑪莉");
foreach (var student in query) {
student.FirstName = "瑪麗亞";
await App.StudentTable.UpdateAsync(student);
}
}
然後,將雲端的資料瑪莉改成aaaa。
圖 23
回到應用程式,按下【Update 瑪麗亞】à【載入資料】按鈕,結果如下:
圖 24
接著按下【Pull】按鈕,應用程式會出現以下畫面:
圖 25
這個訊息方塊的選項是寫在SyncHandler.cs 中的程式,等等我們在解釋這一個部分的程式碼,這裡請按下【使用本地端的版本】。更新完成之後,可以看到雲端資料庫也更新為【瑪麗亞】了。
圖 26
現在讓我們來看看SyncHandler.cs 中的程式,當發生衝突時就會跑到這段程式碼,當我們選擇【使用本地端的版本】,程式會將本地的伺服器端版本改為雲端目前的資料也就是 firstName 為 aaaa。
圖 27
以下是上圖中斷點的程式區段:
operation.Item[MobileServiceSystemColumns.Version ] = serverValue[MobileServiceSystemColumns.Version];
讓雲端目前的版本(serverValue[MobileServiceSystemColumns.Version])與本地的(operation.Item[MobileServiceSystemColumns.Version ])修改前版本相同就可以成功的將【瑪麗亞】寫回雲端了。
以下是衝突處理的完整程式:
public async System.Threading.Tasks.Task<Newtonsoft.Json.Linq.JObject> ExecuteTableOperationAsync(IMobileServiceTableOperation operation)
{
MobileServicePreconditionFailedException error;
do {
error = null;
try {
return await operation.ExecuteAsync();
}
catch (MobileServicePreconditionFailedException ex) {
// pick up the server-side value from the exception.
error = ex;
}
//自訂這裡做衝突處理
if (error != null) {
var serverValue = error.Value;
var dialog = new MessageDialog("你想要如何解決資料差異的衝突?", "請選擇 本地 / 伺服器端 / 取消");
dialog.Commands.Add(new UICommand(LOCAL_VERSION));
dialog.Commands.Add(new UICommand(SERVER_VERSION));
dialog.Commands.Add(new UICommand("取消"));
IUICommand command = await dialog.ShowAsync();
if (command.Label == LOCAL_VERSION) {
operation.Item[MobileServiceSystemColumns.Version ] = serverValue[MobileServiceSystemColumns.Version];
continue;
}
else if (command.Label == SERVER_VERSION) {
return (JObject)serverValue;
}
else {
operation.AbortPush();
}
}
} while (error != null);
return null;
}
若是選擇【使用伺服器端的版本】,則是回傳serverValue ,結果是伺服器版本的資料會被寫到本地端資料表。
總結
開發企業級的行動裝置的應用程式首要問題就是資料存取的離線及衝突的處理,Windows Azure 行動服務本身就是Microsoft 提供給行動開發人員一個雲端便利的選項,搭配使用 SQLite API 可以讓開發人員輕鬆解決資料的離線及衝突。