2016 年 7 月

第 31 卷,第 7 期

本文章是由機器翻譯。

Xamarin - 使用 SQLite 與 Xamarin.Forms 操作本機資料庫

Alessandro Del Del

不用,應用程式會使用資料。這是不只適用於桌面和 Web 應用程式,也適用於行動應用程式,則為 true。在許多情況下,行動裝置應用程式在網路上交換資料,並充分利用雲端儲存體和服務,例如推播通知。不過,有在其行動應用程式只需要在本機儲存資料的情況。使用簡單、 非結構化資料,例如使用者設定和選項,應用程式可以儲存在本機檔案,例如 XML 或文字,或透過不同的開發平台所提供的特定物件內的資訊。如果是複雜的結構化資料,應用程式需要不同的方式來儲存資訊。

好消息是,您可以輕鬆地使用 SQLite 的行動應用程式中包含本機資料庫 (sqlite.org)。SQLite 是開放原始碼的輕量型、 無伺服器的資料庫引擎,可簡化建立本機資料庫,以及在資料上執行作業。資訊會儲存在資料表內,可以撰寫 C# 程式碼和 LINQ 查詢執行資料作業。SQLite 完全符合跨平台開發,因為它是可攜式的資料庫引擎。事實上,預先安裝在 iOS 和 Android,而且它可以輕鬆地部署到 Windows,以及。基於這個理由,SQLite 也是完美的夥伴,來建置跨平台的資料中心行動裝置應用程式需要本機資料庫的 Xamarin.Forms。在本文中我將示範如何使用 Xamarin.Forms,建立行動裝置應用程式目標的 Android、 iOS 和通用 Windows 平台 (UWP) 而且會利用的 SQLite 來儲存和擷取本機資料。我假設您已了解如何使用 Visual Studio 2015; 建立 Xamarin.Forms 應用程式XAML 是什麼。以及如何使用不同的模擬器隨附在各種平台 Sdk 在 Xamarin.Forms 應用程式進行偵錯。如需詳細資訊,您可以閱讀下列文章︰ 「 建置跨平台的 UX,使用 Xamarin.Forms 」 (msdn.com/magazine/mt595754),「 使用 Xamarin.Forms 行動平台共用的 UI 程式碼 」 (msdn.com/magazine/dn904669) 和 「 建置一個使用 C# 和 Xamarin 的跨平台的行動高爾夫球應用程式 」 (msdn.com/magazine/dn630648)。後者會描述如何使用 Microsoft Azure 平台上的資料。這篇文章和範例程式碼根據 Xamarin.Forms 2.0,您安裝 Xamarin 4.0.3 (含) 取得。

啟用 SQLite UWP 應用程式

SQLite 核心引擎已經包含在 iOS 和 Android,但不是在 Windows 上。因此,您必須包含您的應用程式套件的 SQLite 二進位檔。而不是以手動方式包括這類具有每個專案的二進位檔,您可以利用 SQLite 擴充功能的 Visual Studio 2015,可提供與資料庫引擎的先行編譯的二進位檔,以及自動化工作,包括新的專案所需的檔案。我說明之前顯示如何建立新的專案,因為在 IDE 的擴充功能運作層級,不是在專案層級,並且會在每次您在方案中包含的 SQLite 程式庫提供先行編譯的 SQLite 二進位檔。有數個 SQLite 延伸模組,個別針對特定的 Windows 版本,您可以使用下載擴充功能和更新工具在 Visual Studio 2015 中所示, [圖 1

Visual Studio 2015 中的通用 Windows 平台擴充功能下載 SQLite
[圖 1 的 Visual Studio 2015 中的通用 Windows 平台擴充功能下載 SQLite

在此情況下,下載並安裝適用於通用 Windows 平台擴充功能的 SQLite。如此一來,將以 sqlite 的 UWP 應用程式也會包括先行編譯的資料庫引擎二進位檔。如果需要,請重新啟動 Visual Studio 2015,安裝擴充功能之後。

建立範例專案

執行第一件事是建立新的專案 Xamarin 表單為基礎。您使用 Visual Studio 2015 中的專案範本稱為空白應用程式 (Xamarin.Forms 可攜式),則位於 [新增專案] 對話方塊中的 Visual C# 節點的跨平台資料夾 (請參閱 [圖 2)。

在 Visual Studio 2015 中建立新的 Xamarin 表單專案
[圖 2 建立新的 Xamarin Visual Studio 2015 Form 專案

選擇可移植的專案類型,而不是共用類型的原因是您可能想要產生可重複使用的資料存取層內程式庫,而是只在其所屬的方案內的共用專案的範圍。在本文結尾處,我將說明更徹底可攜式類別庫和共用專案之間的差異。

當您按一下 [確定] 時,Visual Studio 2015 會產生新的方案包含 iOS、 Android、 UWP、 Windows 執行階段和 Windows Phone,以及可攜式類別庫 (PCL) 專案為目標的專案。後者是您將在其中撰寫大部分的跨平台特定專案中共用的程式碼。 

安裝 SQLite NuGet 套件

建立專案之後,您需要受管理的方式來存取 SQLite 資料庫。在 Microsoft.NET Framework 中,允許對 SQLite 資料庫工作的許多程式庫,但您需要的是特殊的可攜式程式庫,也能顧及 Xamarin 應用程式。它叫做 SQLite 網路,它是一個開放原始碼和輕量型.NET、 單聲道、 和 Xamarin 應用程式程式庫。它是以名稱為 sqlite-網路-pcl NuGet 套件形式提供。輸入安裝 sqlite net-pcl,或從 Visual Studio 2015,您以滑鼠右鍵按一下 [方案總管] 中的方案名稱,然後選取 [管理方案的 NuGet 封裝啟用 NuGet UI,您可以從 NuGet 封裝管理員主控台,安裝 nuget 方案層級。[圖 3 示範如何尋找及安裝 sqlite net pcl 封裝透過 NuGet UI。

安裝適當的 NuGet 封裝
[圖 3 安裝適當的 NuGet 封裝

現在具有所需的一切,且您已經準備好開始撰寫程式碼。

平台特定程式碼︰ 提供連接字串

由於搭配任何一種資料庫,您的程式碼會存取 SQLite 資料庫透過連接字串,也就是第一件事您需要建置。SQLite 資料庫是位於本機資料夾中的檔案,因為建構連接字串需要資料庫的路徑名稱。雖然大部分您所撰寫的程式碼的各種不同平台共用,Android、 iOS 和 Windows 處理路徑名稱的方式都不同,因此建置連接字串需要平台特定程式碼。然後叫用透過相依性插入的連接字串。

在可攜式專案中,新增名為 IDatabaseConnection.cs 的新介面,並撰寫下列程式碼︰

public interface IDatabaseConnection
{
  SQLite.SQLiteConnection DbConnection();
}

這個介面會公開一個稱為 DbConnection,將每個平台特定專案中實作,而且會傳回適當的連接字串的方法。

下一個步驟實作介面,並傳回適當的連接字串中,根據每個平台特定專案中加入類別範例資料庫中,我將稱之為 CustomersDb.db3。(如果您不熟悉 SQLite,.db3 是識別 SQLite 資料庫檔案副檔名)。 在 LocalDataAccess.Droid 專案中,加入新的類別稱為 DatabaseConnection_Android.cs 和撰寫程式碼所示 [圖 4

[圖 4 在 Android 專案中產生的連接字串

using SQLite;
using LocalDataAccess.Droid;
using System.IO;
[assembly: Xamarin.Forms.Dependency(typeof(DatabaseConnection_Android))]
namespace LocalDataAccess.Droid
{
  public class DatabaseConnection_Android : IDatabaseConnection
  {
    public SQLiteConnection DbConnection()
    {
      var dbName = "CustomersDb.db3";
      var path = Path.Combine(System.Environment.
        GetFolderPath(System.Environment.
        SpecialFolder.Personal), dbName);
      return new SQLiteConnection(path);
    }
  }
}

呼叫 Xamarin.Forms.Dependency 屬性會指出指定的類別會實作必要的介面。這個屬性會套用在命名空間層級與組件關鍵字。在 Android 上,資料庫檔案必須儲存在個人資料夾中,因此資料庫的路徑名稱由檔案名稱 (CustomersDb.db3) 和個人的資料夾路徑。產生的路徑名稱是指派做為 SQLiteConnection 類別的建構函式的參數,並傳回呼叫端。在 iOS 上,您可以使用相同的 API,但 SQLite 資料庫所在之資料夾是 Personal\Library。

現在,加入新的類別稱為 DatabaseConnection_iOS.cs 加入 iOS 專案,並撰寫程式碼所示 [圖 5

[圖 5 iOS 專案中產生連接字串

using LocalDataAccess.iOS;
using SQLite;
using System;
using System.IO;
[assembly: Xamarin.Forms.Dependency(typeof(DatabaseConnection_iOS))]
namespace LocalDataAccess.iOS
{
  public class DatabaseConnection_iOS
  {
    public SQLiteConnection DbConnection()
    {
      var dbName = "CustomersDb.db3";
      string personalFolder =
        System.Environment.
        GetFolderPath(Environment.SpecialFolder.Personal);
      string libraryFolder =
        Path.Combine(personalFolder, "..", "Library");
      var path = Path.Combine(libraryFolder, dbName);
      return new SQLiteConnection(path);
    }
  }
}

在 Windows 10 SQLite 資料庫所在的應用程式本機資料夾中。因為工作而不是 System.IO Windows.Storage 命名空間的類別,是不同於其他平台,您用來存取本機資料夾的 API。加入新的類別稱為 DatabaseConnection_UWP.cs 至通用 Windows 專案中,並撰寫程式碼所示 [圖 6

[圖 6 在通用 Windows 專案中產生的連接字串

using SQLite;
using Xamarin.Forms;
using LocalDataAccess.UWP;
using Windows.Storage;
using System.IO;
[assembly: Dependency(typeof(DatabaseConnection_UWP))]
namespace LocalDataAccess.UWP
{
  public class DatabaseConnection_UWP : IDatabaseConnection
  {
    public SQLiteConnection DbConnection()
    {
      var dbName = "CustomersDb.db3";
      var path = Path.Combine(ApplicationData.
        Current.LocalFolder.Path, dbName);
      return new SQLiteConnection(path);
    }
  }
}

這次應用程式的本機資料夾路徑會傳回 Windows.Storage.ApplicationData.Current.LocalFolder.Path 屬性加上要透過 SQLiteConnection 物件傳回的連接字串的資料庫名稱。現在您已經撰寫可產生的應用程式執行所在的平台為基礎的適當連接字串的平台特定程式碼。從現在起,將會共用您的程式碼。下一個步驟實作的資料模型。

撰寫資料模型

應用程式的目標是使用簡化的 SQLite 資料庫內儲存的客戶清單,並支援資料上進行作業。第一件事,此時是代表的客戶,將會對應到資料庫中資料表的類別。在可攜式專案中,加入名為 models 的類別。這個類別必須實作 INotifyPropertyChanged 介面,以通知呼叫端的資料會儲存變更。它會使用 SQLite 命名空間中的特殊屬性,加上註解與驗證規則的屬性和其他資訊非常接近 System.ComponentModel.DataAnnotations 命名空間中的資料註解的方式。[圖 7 顯示範例客戶類別。

[圖 7 實作資料模型

using SQLite;
using System.ComponentModel;
namespace LocalDataAccess
{
  [Table("Customers")
  public class Customer: INotifyPropertyChanged
  {
    private int _id;
    [PrimaryKey, AutoIncrement]
    public int Id
    {
      get
      {
        return _id;
      }
      set
      {
        this._id = value;
        OnPropertyChanged(nameof(Id));
      }
    }
    private string _companyName;
    [NotNull]
    public string CompanyName
    {
      get
      {
        return _companyName;
      }
      set
      {
        this._companyName = value;
        OnPropertyChanged(nameof(CompanyName));
      }
    }
    private string _physicalAddress;
    [MaxLength(50)]
    public string PhysicalAddress
    {
      get
      {
        return _physicalAddress;
      }
      set
      {
        this._physicalAddress=value;
        OnPropertyChanged(nameof(PhysicalAddress));
      }
    }
    private string _country;
    public string Country
    {
      get
      {
        return _country;
      }
      set
      {
        _country = value;
        OnPropertyChanged(nameof(Country));
      }
    }
    public event PropertyChangedEventHandler PropertyChanged;
    private void OnPropertyChanged(string propertyName)
    {
      this.PropertyChanged?.Invoke(this,
        new PropertyChangedEventArgs(propertyName));
    }
  }
}

這個類別的重點註釋具有 SQLite 屬性的物件。資料表屬性可讓您指定資料表名稱,在此案例的客戶。這不是必要項目,但如果您未指定,SQLite 就會產生新的資料表,在此情況下為基礎的類別名稱,而客戶。因此,為了保持一致性,程式碼會產生新的資料表與複數名稱。套用至的 Id 屬性的 PrimaryKey 和 AutoIncrement 屬性將此主索引鍵自動遞增的客戶資料表中。NotNull 屬性套用至 CompanyName 屬性標記為必要項目,這表示如果屬性值為 null 的資料存放區驗證將會失敗。套用至 PhysicalAddress 屬性的 MaxLength 屬性指出屬性值的最大長度。另一個有趣的屬性是資料行,您可以套用至屬性名稱,以提供不同的資料行名稱在資料庫中。

實作資料存取

撰寫簡單的資料模型,就必須提供對資料執行作業的方法的類別。為避免混淆,因為這是一個簡單的範例,將不會; 使用 Model View ViewModel (MVVM) 方法並非所有的讀取器有這種模式的經驗,並在任何情況下,更可以用於大型專案。只要您當然可以重新撰寫範例。

讓我們開始新的類別稱為 CustomersDataAccess.cs 在可攜式的專案中,需要下列 using 指示詞︰

using SQLite;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
using System.Collections.ObjectModel;

首先您需要在此類別是私用欄位來儲存連接字串和用於資料作業,以避免衝突資料庫上實作鎖定的物件︰

private SQLiteConnection database;
private static object collisionLock = new object();

更具體來說,鎖定應該有下列形式︰

// Use locks to avoid database collisions
lock(collisionLock)
{
  // Data operation here ...
}

使用 Xamarin.Forms,使用 XAML 建置的 UI,您可以利用資料繫結來顯示和輸入資訊。基於這個理由,您要公開的方式,XAML 可以在使用中的資料。最好的方法公開 (expose) 類型 ObservableCollection < 客戶 > 像這樣的屬性︰

public ObservableCollection<Customer> Customers { get; set; }

ObservableCollection 類型具有內建支援變更通知。因此,它是最適當的集合中以 XAML 為基礎的平台的資料繫結。

現在就開始實作類別建構函式,也會負責叫用 DbConnection 方法,它會傳回適當的連接字串中所示的平台特定實作 [圖 8

[圖 8 實作類別建構函式

public CustomersDataAccess()
{
  database =
    DependencyService.Get<IDatabaseConnection>().
    DbConnection();
  database.CreateTable<Customer>();
  this.Customers =
    new ObservableCollection<Customer>(database.Table<Customer>());
  // If the table is empty, initialize the collection
  if (!database.Table<Customer>().Any())
  {
    AddNewCustomer();
  }
}

請注意程式碼如何叫用 DependencyService.Get 方法。這是泛型方法,以在此情況下傳回提供的泛型類型,IDatabaseConnection 的平台特定實作。使用此方法根據相依性插入時,程式碼叫用 DbConnection 方法的平台特定實作。Xamarin 執行階段就會知道如何解決應用程式執行所在的作業系統為基礎的方法呼叫。叫用這個方法也會造成要建立如果找不到其中一個資料庫。SQLiteConnection 物件會公開稱為的 CreateTable < T >,其中的泛型型別是模型類別,客戶在本例中的泛型方法。這個簡單的程式碼行,您可以建立新的 [客戶] 資料表。如果資料表已存在,則不會覆寫。此程式碼也會初始化客戶的屬性。它會叫用資料表 < T > 泛型方法,從 SQLiteConnection,其中的泛型型別仍然是模型類別。資料表 < T > 會傳回 TableQuery < T > < T > IEnumerable 介面的實作類型的物件,並也可使用 LINQ 查詢。實際傳回的結果包含 < T > 物件的清單不過,繫結 TableQuery 物件 ui 直接不適當的方式呈現資料,因此新 ObservableCollection < 客戶 > 根據傳回的結果會產生並指派給客戶的屬性。程式碼也會叫用方法,稱為 AddNewCustomer,如果資料表是空的定義如下︰

public void AddNewCustomer()
{
  this.Customers.
    Add(new Customer
    {
      CompanyName = "Company name...",
      PhysicalAddress = "Address...",
      Country = "Country..."
    });
}

這個方法只會將新客戶加入到客戶集合,使用預設屬性值,並避免繫結至空的集合。 

查詢資料

查詢資料是非常重要。SQLite 有基本上是實作查詢的兩種方式。第一個使用 LINQ,對資料表 < T > 方法,這是 TableQuery < T > 類型的物件,實作 IEnumerable < T > 介面在引動過程的結果。第二個叫用一個稱為 SQLiteConnection.Query < T >,後者會採用引數的型別字串,代表以 SQL 撰寫查詢的方法。中的程式碼 [圖 9 示範如何篩選客戶的國家/地區、 使用這兩種方法的清單。

[圖 9 篩選依國家/地區的客戶清單

public IEnumerable<Customer> GetFilteredCustomers(string countryName)
{
  lock(collisionLock)
  {
    var query = from cust in database.Table<Customer>()
                where cust.Country == countryName
                select cust;
    return query.AsEnumerable();
  }
}
public IEnumerable<Customer> GetFilteredCustomers()
{
  lock(collisionLock)
  {
    return database.Query<Customer>(
      "SELECT * FROM Item WHERE Country = 'Italy'").AsEnumerable();
  }
}

GetFilteredCustomers 的第一個多載會傳回篩選根據國家/地區名稱,做為方法引數提供的資料將 LINQ 查詢的結果。第二個多載會叫用來直接執行 SQL 查詢的查詢。這個方法會預期將泛型清單,其泛型型別是相同的傳遞給查詢的結果。如果查詢失敗,則會擲回 SQLiteException。當然,您可以擷取使用 LINQ 或擴充方法,如下列程式碼的客戶清單上叫用 FirstOrDefault,擷取指定之的客戶執行個體根據其識別碼指定的物件執行個體︰

public Customer GetCustomer(int id)
{
  lock(collisionLock)
  {
    return database.Table<Customer>().
      FirstOrDefault(customer => customer.Id == id);
  }
}

執行 CRUD 作業

建立、 讀取、 更新和刪除 (CRUD) 作業也是非常重要。讀取資料通常會使用上一節中所述的方法,所以現在我將討論如何建立、 更新並刪除 SQLite 資料庫中的資訊。SQLiteConnection 物件公開 Insert、 InsertAll、 更新和 UpdateAll 方法來插入或更新資料庫中的物件。InsertAll 和 UpdateAll 執行插入或更新作業實作 IEnumerable < T > 傳遞的引數的集合。批次中執行 insert 或 update 作業,這兩種方法也允許執行的作業為交易。請記住,雖然 InsertAll 需要資料庫中存在集合中的沒有項目,UpdateAll 需要已經在集合中的所有項目存在於資料庫。在此情況下,我有 ObservableCollection < 客戶 >,可包含從資料庫擷取的物件及新物件加入透過 UI,但尚未儲存或暫止的編輯現有的物件。基於這個理由,不建議使用 InsertAll 和 UpdateAll。較理想的方法只會檢查 「 客戶 」 類別的執行個體是否有一個 id。若是如此,請執行個體已經存在資料庫中,您只需要更新。識別碼為零,如果執行個體不存在於資料庫中。因此,它必須先儲存。中的程式碼 圖 10 示範如何插入或更新根據先前考量 Customer 物件的單一執行個體。

[圖 10 插入或更新於客戶類別識別碼存在客戶 ObjectDepending 的單一執行個體

public int SaveCustomer(Customer customerInstance)
{
  lock(collisionLock)
  {
    if (customerInstance.Id != 0)
    {
      database.Update(customerInstance);
      return customerInstance.Id;
    }
    else
    {
      database.Insert(customerInstance);
      return customerInstance.Id;
    }
  }
}

中的程式碼 [圖 11, ,相反地,示範如何插入或更新客戶的所有執行個體。

[圖 11 插入或更新客戶的所有執行個體

public void SaveAllCustomers()
{
  lock(collisionLock)
  {
    foreach (var customerInstance in this.Customers)
    {
      if (customerInstance.Id != 0)
      {
        database.Update(customerInstance);
      }
      else
      {
        database.Insert(customerInstance);
      }
    }
  }
}

Insert 和 Update 方法會傳回整數,表示新增或更新的資料列數目。插入也提供接受字串,包含要插入的資料列,針對執行的其他 SQL 陳述式的多載。此外,值得一提,Insert 會自動更新,所參考,您已在此情況下是主索引鍵 Customer.Id 對應的商務物件中的屬性。SQLiteConnection 類別還會公開 Delete < T > 和 < T > DeleteAll 永久刪除資料表中的一個或所有物件的方法。刪除作業無法復原,因此要注意的正在做什麼。下列程式碼會實作一個稱為刪除指定之的客戶的執行個體從記憶體中的客戶集合和資料庫的 DeleteCustomer 方法︰

public int DeleteCustomer(Customer customerInstance)
{
  var id = customerInstance.Id;
  if (id != 0)
  {
    lock(collisionLock)
    {
      database.Delete<Customer>(id);
    }
  }
  this.Customers.Remove(customerInstance);
  return id;
}

指定之的客戶的識別碼,它是否存在資料庫中,將會永久刪除,因此也從客戶集合中移除。刪除 < T > 會傳回整數,表示已刪除的資料列數目。您也可以永久刪除所有物件從一個資料表。當然,您可以叫用的 DeleteAll < T >,其中的泛型型別是您的商務物件,例如客戶,但我想要顯示替代的方法,以便取得其他成員的知識。SQLiteConnection 類別會公開一個稱為 DropTable < T >,會永久終結資料庫中的資料表的方法。比方說,您可能會實作資料表刪除,如下所示︰

public void DeleteAllCustomers()
{
  lock(collisionLock)
  {
    database.DropTable<Customer>();
    database.CreateTable<Customer>();
  }
  this.Customers = null;
  this.Customers = new ObservableCollection<Customer>
    (database.Table<Customer>());
}

程式碼會刪除 [客戶] 資料表,然後會建立一個,和最後清除並重新建立客戶集合。圖 12 顯示 CustomersDataAccess.cs 類別的完整清單。

圖 12 CustomersDataAccess.cs 類別

using SQLite;
using System.Collections.Generic;
using System.Linq;
using Xamarin.Forms;
using System.Collections.ObjectModel;
namespace LocalDataAccess
{
  public class CustomersDataAccess
  {
    private SQLiteConnection database;
    private static object collisionLock = new object();
    public ObservableCollection<Customer> Customers { get; set; }
    public CustomersDataAccess()
    {
      database =
        DependencyService.Get<IDatabaseConnection>().
        DbConnection();
      database.CreateTable<Customer>();
      this.Customers =
        new ObservableCollection<Customer>(database.Table<Customer>());
      // If the table is empty, initialize the collection
      if (!database.Table<Customer>().Any())
      {
        AddNewCustomer();
      }
    }
    public void AddNewCustomer()
    {
      this.Customers.
        Add(new Customer
        {
          CompanyName = "Company name...",
          PhysicalAddress = "Address...",
          Country = "Country..."
        });
    }
    // Use LINQ to query and filter data
    public IEnumerable<Customer> GetFilteredCustomers(string countryName)
    {
      // Use locks to avoid database collitions
      lock(collisionLock)
      {
        var query = from cust in database.Table<Customer>()
                    where cust.Country == countryName
                    select cust;
        return query.AsEnumerable();
      }
    }
    // Use SQL queries against data
    public IEnumerable<Customer> GetFilteredCustomers()
    {
      lock(collisionLock)
      {
        return database.
          Query<Customer>
          ("SELECT * FROM Item WHERE Country = 'Italy'").AsEnumerable();
      }
    }
    public Customer GetCustomer(int id)
    {
      lock(collisionLock)
      {
        return database.Table<Customer>().
          FirstOrDefault(customer => customer.Id == id);
      }
    }
    public int SaveCustomer(Customer customerInstance)
    {
      lock(collisionLock)
      {
        if (customerInstance.Id != 0)
        {
          database.Update(customerInstance);
          return customerInstance.Id;
        }
        else
        {
          database.Insert(customerInstance);
          return customerInstance.Id;
        }
      }
    }
    public void SaveAllCustomers()
    {
      lock(collisionLock)
      {
        foreach (var customerInstance in this.Customers)
        {
          if (customerInstance.Id != 0)
          {
            database.Update(customerInstance);
          }
          else
          {
            database.Insert(customerInstance);
          }
        }
      }
    }
    public int DeleteCustomer(Customer customerInstance)
    {
    var id = customerInstance.Id;
      if (id != 0)
      {
        lock(collisionLock)
        {
          database.Delete<Customer>(id);
        }
      }
      this.Customers.Remove(customerInstance);
      return id;
    }
    public void DeleteAllCustomers()
    {
      lock(collisionLock)
      {
        database.DropTable<Customer>();
        database.CreateTable<Customer>();
      }
      this.Customers = null;
      this.Customers = new ObservableCollection<Customer>
        (database.Table<Customer>());
    }
  }
}

簡單的 UI 資料繫結

現在您已在模型和資料存取層,您需要顯示並編輯資料的 UI。如您所知,使用 Xamarin.Forms UI 可以撰寫 C# 或 XAML 中,但後者,則提供更好的區隔 UI 和程序的程式碼之間,並提供的 UI 以階層方式組織,立即感覺,所以我選擇這份文件。值得一提的是,使用 Xamarin.Forms 2.0,您也可以讓效能最佳化和編譯階段錯誤檢查的 XAML 編譯 (XamlC)。如需 XamlC 進一步詳細資訊,請瀏覽 bit.ly/24BSUC8

現在我們要撰寫簡單的頁面會顯示某些按鈕具有資料的清單。Xamarin.Forms,在頁面是共用的項目,它們加入至可攜式的專案。若要這樣做,請在 [方案總管] 可移植的專案上按一下滑鼠右鍵然後選取 [新增 |新項目。在 [加入新項目] 對話方塊中,找出跨平台] 節點,然後選取 Form Xaml 頁面範本中所示 圖 13。命名新的頁面 CustomersPage.cs,然後按一下 [新增]。

加入新的頁面以 XAML 為基礎
將新頁面的 [圖 13 根據 XAML

以顯示客戶清單,請將資料繫結至客戶集合 CustomersDataAccess 類別所公開的 ListView 控制項由簡單的 UI。這些項目的 DataTemplate 包含四個項目控制項,每個資料繫結至客戶模型類別的屬性。如果您有其他以 XAML 為基礎的平台,例如 Windows Presentation Foundation (WPF) 和 UWP 使用經驗,您可以考慮為 TextBox 控制項對應的項目。項目控制項會分組在 StackLayout 面板,內含在 ViewCell 容器內。對於來自 WPF 和 UWP,StackLayout 相當 Xamarin 的 StackPanel 容器。ViewCell 可讓您建立自訂的資料格內的項目控制項 (例如 ListView)。您會發現,我使用的項目控制項與 [IsEnabled] 屬性指定為 False Customer.Id 屬性,而不是一個 Label 控制項,也就是唯讀的本質。您可能記得,當您叫用 SQLiteConnection.Insert 方法,因為這會更新對應您的模型中的主索引鍵,因此 UI 應該要能夠都會自動反映這項變更的屬性。不幸的是,Label 控制項不會自行更新與新的值,而項目控制項,並因此,項目已使用,但設定成唯讀。

UI 的第二個部分組成 Toolbar 按鈕,提供簡單且共用的方式提供方便的功能表列,將可在所有平台上的使用者互動。為了簡單起見,這些按鈕會實作次要區域中的功能表列,而不需要特定平台的圖示。圖 14 UI 會顯示完整的程式碼。

[圖 14 CustomersPage 使用者介面

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LocalDataAccess.CustomersPage">
  <ListView x:Name="CustomersView"
            ItemsSource="{Binding Path=Customers}"
            ListView.RowHeight="150">
    <ListView.ItemTemplate>
      <DataTemplate>
        <ViewCell>
          <StackLayout Orientation="Vertical">
            <Entry Text="{Binding Id}" IsEnabled="False"/>
            <Entry Text="{Binding CompanyName}" />
            <Entry Text="{Binding PhysicalAddress}"/>
            <Entry Text="{Binding Country}"/>
          </StackLayout>
        </ViewCell>          
      </DataTemplate>
    </ListView.ItemTemplate>
  </ListView>
<ContentPage.ToolbarItems>
  <ToolbarItem Name="Add" Activated="OnAddClick"
               Priority="0" Order="Secondary" />
  <ToolbarItem Name="Remove" Activated="OnRemoveClick"
               Priority="1" Order="Secondary" />
  <ToolbarItem Name="Remove all" Activated="OnRemoveAllClick"
               Priority="2" Order="Secondary" />
  <ToolbarItem Name="Save" Activated="OnSaveClick"
               Priority="3" Order="Secondary" />
</ContentPage.ToolbarItems>
</ContentPage>

請注意每個 ToolbarItem 為何有 Order 屬性指派為次要資料庫。如果您想要讓它們可在工具列的主要區域,並提供一些圖示,變更為 「 主要 」。此外,優先權屬性允許指定 ToolbarItem 時才會顯示在工具列上,按一下事件,就可以比較已啟動及需要的事件處理常式的順序。

下一個步驟撰寫 C# 程式碼來具現化 CustomersDataAccess 類別、 資料繫結物件,並對資料執行作業。圖 15 示範 C# 程式碼後的置頁面 (如需詳細資訊的註解,請參閱)。

圖 15 CustomersPage 程式碼後置

using System;
using System.Linq;
using Xamarin.Forms;
namespace LocalDataAccess
{
  public partial class CustomersPage : ContentPage
  {
    private CustomersDataAccess dataAccess;
    public CustomersPage()
    {
      InitializeComponent();
      // An instance of the CustomersDataAccessClass
      // that is used for data-binding and data access
      this.dataAccess = new CustomersDataAccess();
    }
    // An event that is raised when the page is shown
    protected override void OnAppearing()
    {
      base.OnAppearing();
      // The instance of CustomersDataAccess
      // is the data binding source
      this.BindingContext = this.dataAccess;
    }
    // Save any pending changes
    private void OnSaveClick(object sender, EventArgs e)
    {
      this.dataAccess.SaveAllCustomers();
    }
    // Add a new customer to the Customers collection
    private void OnAddClick(object sender, EventArgs e)
    {
      this.dataAccess.AddNewCustomer();
    }
    // Remove the current customer
    // If it exist in the database, it will be removed
    // from there too
    private void OnRemoveClick(object sender, EventArgs e)
    {
      var currentCustomer =
        this.CustomersView.SelectedItem as Customer;
      if (currentCustomer!=null)
      {
        this.dataAccess.DeleteCustomer(currentCustomer);
      }
    }
    // Remove all customers
    // Use a DisplayAlert object to ask the user's confirmation
    private async void OnRemoveAllClick(object sender, EventArgs e)
    {
      if (this.dataAccess.Customers.Any())
      {
        var result =
          await DisplayAlert("Confirmation",
          "Are you sure? This cannot be undone",
          "OK", "Cancel");
        if (result == true)
        {
          this.dataAccess.DeleteAllCustomers();
          this.BindingContext = this.dataAccess;
        }
      }
    }
  }
}

Messageencodingbindingelement 屬性就相當於在 WPF 和 UWP DataContext,而代表目前頁面的資料來源。

測試應用程式使用模擬器

現在就開始測試應用程式。在 App.cs 檔案中,您需要變更 [啟動] 頁面。當您建立的 Xamarin.Forms 專案時,Visual Studio 2015 會產生以程序程式碼撰寫,並指派給名為 MainPage 物件的頁面。這項指派是在應用程式類別建構函式,所以開啟 App.cs 並將應用程式建構函式,如下所示︰

public App()
{
  // The root page of your application
  MainPage = new NavigationPage(new CustomersPage());
}

您可能會感到驚訝,CustomersPage 的執行個體不直接指派給 MainPage;相反地,它會封裝為 NavigationPage 類別的執行個體的參數。原因是,使用 NavigationPage 是唯一的方式來顯示功能表列在 Android 上,但這並不會完全不會影響 UI 行為。根據您想要測試應用程式平台,請選取 [Visual Studio 中的 [標準] 工具列中的 [啟始專案和適當的模擬器,然後按 F5。圖 16 顯示執行 Android 和 Windows 10 行動裝置應用程式。

在不同平台上執行範例應用程式
圖 16 不同平台上執行的範例應用程式

請注意如何工具列項目正確地顯示在功能表列中,您可以使用方式資料相同的方式在不同的裝置和作業系統上。

撰寫使用共用專案的平台特定程式碼

共用程式碼是 Xamarin.Forms 的主要概念。事實上,不會利用平台特定 Api 的所有程式碼可以寫入一次,而且共用到 iOS、 Android 和 Windows 的專案。當您建立的 Xamarin.Forms 專案時,Visual Studio 2015 提供空白的應用程式 (Xamarin.Forms 可攜式) 和 (Xamarin.Forms 共用) 的空白應用程式專案範本,根據 PCLs 和共用專案中,分別。一般而言,在 Visual Studio 中您可以共用程式碼使用任一 PCLs,產生可重複使用.dll 程式庫的多個平台為目標,但不允許寫入平台特定程式碼或共用的專案,它不會產生的組件,因此其範圍僅限於其所屬的方案。共用的專案,並允許撰寫平台特定程式碼。

Xamarin.Forms,在 Visual Studio 2015 會產生新的方案,它會新增一個 PCL 專案或根據您選取的範本共用的專案。在這兩種情況下,選取的範本會是您將共用的程式碼的位置。但可攜式專案中程式碼將擁有的 iOS、 Android 和 Windows 專案,其成員會在特定平台實作,然後透過相依性插入叫用的介面。這是您已了解本文中。

在共用專案中,您可以使用條件式前置處理器指示詞 (#if、 #else #endif) 並輕鬆地讓您的環境變數了解您的應用程式執行所在的平台為何,您可以撰寫在共用專案中的平台特定程式碼直接。在本文中所述範例應用程式的情況下,連接字串是使用平台特定的 Api 來建構。如果您使用共用的專案,您可以撰寫程式碼所示 圖 17 直接在共用專案中。  請記住共用的專案,並不支援的 NuGet 套件,因此您必須將包含 SQLite.cs 檔案 (GitHub 上的 bit.ly/1QU8uiR)。

[圖 17 撰寫在共用專案中使用條件式前置處理器指示詞的連接字串

private string databasePath {
  get {
    var dbName = "CustomersDb.db3";
    #if __IOS__
    string folder = Environment.GetFolderPath
      (Environment.SpecialFolder.Personal);
    folder = Path.Combine (folder, "..", "Library");
    var databasePath = Path.Combine(folder, dbName);
    #else
    #if __ANDROID__
    string folder = Environment.GetFolderPath
      (Environment.SpecialFolder.Personal);
    var databasePath = Path.Combine(folder, dbName);
    #else  // WinPhone
    var databasePath =
      Path.Combine(Windows.Storage.ApplicationData.Current.
      LocalFolder.Path, dbName);
    #endif
    #endif
    return databasePath;
  }
}

如您所見,您會使用 #if 和 #else 指示詞來偵測的平台應用程式正在執行。每個平台由 __IOS__、 __ANDROID__ 或 __WINPHONE__ 環境變數,其中 __WINPHONE__ 的目標是 Windows 8.x,Windows Phone 8.x 和 UWP。選擇可攜式類別庫和共用的專案之間完全取決於您的需求。可攜式類別庫是可重複使用,而且需要共用及平台特定程式碼非常清楚地分隔。共用的專案可讓您撰寫平台特定程式碼,以及共用程式碼,但是它們不會產生可重複使用程式庫,而且它們更難維護程式碼基底成長非常相似時。

進一步的改進功能

這篇文章中所述的範例應用程式可以在許多方面的確獲得改善。比方說,可能會想要實作 MVVM 模式和公開 (expose) 至 UI 的命令,而不是處理 click 事件,以及您可以考慮將工具列項目移至功能表列,提供平台專屬圖示中的主要區域。從資料的觀點來看,您可能需要使用關聯性和外部索引鍵。處理兩者皆擁有 SQLite 程式庫不是很容易,因為您可能要考慮 SQLite Net 延伸模組程式庫 (bit.ly/24yhhnP),您需要使用更進階的案例和關聯性與開放原始碼專案,簡化了 C# 程式碼。這是只有小型清單可能會尋求進一步的研究的增強功能。

總結

在許多情況下,行動裝置應用程式需要本機資料存放區。Sqlite,Xamarin.Forms 應用程式可以輕鬆地管理本機資料庫時使用的開放原始碼,無伺服器,以及支援 C# 和 LINQ 查詢的可攜式引擎。SQLite 提供非常直覺性的物件,使用資料表和資料庫物件的任何平台上實作本機資料存取很容易。看看 SQLite 文件 (sqlite.org/docs.html) 的進一步資訊以及其他案例。


Alessandro Del Sole 2008年之後已經是 Microsoft MVP。 獎勵年五倍的 MVP,他著有許多線上叢書 》、 電子書、 解說影片及有關使用 Visual Studio.NET 開發文件。Del Sole 擔任方案開發人員專家大腦 Sys (大腦-sys.it),將焦點放在.NET 開發訓練和諮詢。您也可以關注他的 Twitter: @progalex

衷心感謝以下技術專家對本文的審閱: Kevin 艾和 Sara Silva