2016 年 7 月

第 31 卷,第 7 期

此文章由机器翻译。

Xamarin - 在 Xamarin.Forms 中通过 SQLite 使用本地数据库

通过 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 应用程序。有关详细信息,您可以阅读以下文章︰ "生成借助 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 获取。

启用对 UWP 应用程序的 SQLite

SQLite 核心引擎已包含在 iOS 和 Android,但不是在 Windows 中。因此,您需要包括与你的应用程序包的 SQLite 二进制文件。而不是手动包括此类具有每个项目的二进制文件,您可以利用 SQLite 扩展 Visual Studio 2015 中,为数据库引擎提供预编译的二进制文件并自动执行的任务包括与新项目所需的文件。我将介绍这显示了如何创建新项目,因为在 IDE 的扩展工作级别,不是在项目级别,并将提供的预编译的 SQLite 二进制文件,每次在您的解决方案中包括的 SQLite 库之前。有几个 SQLite 扩展中,每个目标可以使用下载的扩展和更新工具在 Visual Studio 2015 中,如中所示的特定 Windows 版本 图 1

下载 SQLite for Visual Studio 2015 中的通用 Windows 平台扩展
图 1 下载 SQLite for Visual Studio 2015 中的通用 Windows 平台扩展

在这种情况下,下载并安装 SQLite for 通用 Windows 平台扩展。这样做之后,使用 SQLite 的 UWP 应用程序还将包括预编译的数据库引擎的二进制文件。如有必要,请安装扩展后重启 Visual Studio 2015。

创建示例项目

第一个要执行的操作是创建基于 Xamarin 窗体的新项目。Visual Studio 2015 中使用的项目模板称为空白应用 (Xamarin.Forms Portable),位于的跨平台文件夹中的 Visual C# 节点在新建项目对话框 (请参阅 图 2)。

在 Visual Studio 2015 中创建新的 Xamarin 窗体项目
图 2 创建新的 Xamarin Visual Studio 2015 中的窗体项目

选择可移植的项目类型而不是共享类型的原因是,您可能想要生成一个库内的可重用数据访问层而共享项目的作用域是仅在它所属的解决方案。末尾的文章中,我将介绍更彻底的可移植库和 Shared 项目之间的差异。

当您单击确定时,Visual Studio 2015 会生成一个新解决方案,包含面向 iOS、 Android、 UWP、 Windows 运行时和 Windows Phone 中,以及可移植类库 (PCL) 项目的项目。后者是,您将在其中编写的大多数将特定于平台的项目之间共享代码。 

安装 SQLite NuGet 包

创建项目后,您需要管理的方式来访问 SQLite 数据库。在 Microsoft.NET Framework 中,允许使用 SQLite 数据库的多个库,但您所需是一种特殊的可移植库,还针对 Xamarin 应用程序。它调用 SQLite 的网络,但它是一种开放源代码和用于.NET、 单声道和 Xamarin 应用程序的轻型库。它是具有名称 sqlite net 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。

现在,添加到 iOS 项目名 DatabaseConnection_iOS.cs 的新类并编写代码所示 图 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 数据库驻留在应用程序的本地文件夹。使用本地文件夹的访问的 API 是不同于其他平台,,因为在使用而不是 System.IO Windows.Storage 命名空间中的类。添加新的类称为 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);
    }
  }
}

这一次与要通过 SQLiteConnection 对象返回的连接字符串的数据库名称组合的 Windows.Storage.ApplicationData.Current.LocalFolder.Path 属性返回应用程序的本地文件夹路径。现在您已编写允许基于在其运行该应用程序的平台的正确的连接字符串生成的特定于平台的代码。从现在起,将共享您的代码。下一步实现数据模型。

编写数据模型

应用程序的目标是能够使用简化的存储在 SQLite 数据库中的客户列表并支持对数据的操作。具有所需的第一件事此时是一个类来表示将映射到数据库的表中的客户。在可移植的项目中,添加一个名为 Customer.cs 类。此类必须实现 INotifyPropertyChanged 接口,以通知它所存储的数据中的更改的调用方。它将使用 SQLite 命名空间中的特殊属性进行批注的验证规则的属性和其他信息非常接近 System.ComponentModel.DataAnnotations 命名空间中的数据批注的方式。图 7 演示示例 Customer 类。

图 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 和自动增量属性使其主键具有自动增量 Customers 表中。应用于 CompanyName 属性的 NotNull 属性将其标记为必需的这意味着,如果属性值为 null,数据存储区验证将失败。应用于 PhysicalAddress 属性的 MaxLength 属性指示的属性值的最大长度。另一个有趣的属性是可应用于属性名来提供不同的列名称在数据库中的列。

实现数据访问

在编写简单的数据模型之后, 您将需要一个类,提供对数据执行操作的方法。为清楚起见,因为这是一个简单的示例,我不会; 使用模型-视图-视图模型 (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 可以使用它的方式。最好的方法公开了像下面这样的 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 >,其中泛型类型是您的模型类,客户在这种情况下泛型方法。使用此简单的代码行,您将创建新的 Customers 表。如果表已存在,则不会被覆盖。此代码还初始化您的客户属性。它调用的 SQLiteConnection,泛型类型仍是 model 类从表 < T > 泛型方法。表 < T > 将返回类型 TableQuery < T > 实现 IEnumerable < T > 接口的对象,也可以使用 LINQ 查询。< T > 对象的列表包含,实际返回的结果但是,绑定 TableQuery 对象传递给 UI 直接不是显示数据,以便生成基于返回的结果新 ObservableCollection < 客户 > 并将其分配给客户属性的正确方法。代码还会调用调用 AddNewCustomer,如果表为空,则按以下方式定义的方法︰

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

此方法只需将新客户添加到具有默认属性值的客户集合,并可避免绑定到一个空集合。 

查询数据

查询数据是非常重要。SQLite 提供实质上是两种方法的实现的查询。第一个使用 LINQ 针对对表 < T > 方法,这是一个对象类型 < T > TableQuery 并实现 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 并检索基于其 id 的指定的客户实例中所示︰

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 > 作为参数传递的集合上的操作。插入或更新操作执行到批处理中,并且这两种方法还允许执行该操作作为事务。请记住,尽管 InsertAll 要求在集合中的没有项目存在于数据库,UpdateAll 要求所有的项集合中已存在于数据库。在这种情况下,我有一个可包含从数据库中检索的对象和对现有对象添加通过尚未保存,UI 或挂起的编辑的新对象的 ObservableCollection < 客户 >。出于此原因,不建议使用 InsertAll 和 UpdateAll。一个不错的方法只需检查是否 Customer 类的实例有一个 id。如果是这样,该实例中已存在数据库因此只需要更新。Id 为零,如果实例不存在于该数据库。因此,必须保存它。中的代码 图 10 演示如何插入或更新基于上一个需要考虑的 Customer 对象的单个实例。

图 10 插入或更新上的客户类 ID 存在客户 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 语句的字符串。此外,值得一提的是,插入自动更新,通过引用,您已映射到这种情况下是主键,to Customer.Id 的业务对象中的属性。SQLiteConnection 类还公开删除 < T > 和 DeleteAll < T > 方法,从一个表中永久删除一个或所有对象。删除操作是不可逆的因此请记住您所做。下面的代码实现了一个名为从内存中的客户集合和数据库中删除指定的客户实例的 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;
}

如果指定的客户有一个 id,它存在于数据库中,因此它将永久删除,并且还将从 Customers 集合中删除。删除 < 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 与数据绑定

现在,您拥有的模型和数据访问层,您需要用于显示和编辑数据的用户界面。如您所知,Xamarin.Forms 与用户界面可以编写与 C# 或 XAML 中,但后者提供在 UI 和程序代码之间的更好地分离,并且提供您立即感觉的用户界面的分层组织,这就是我在本文的选择。值得一提的是,借助 Xamarin.Forms 2.0,您还可以启用性能优化和编译时错误检查的 XAML 编译 (XamlC)。有关 XamlC 的进一步详细的信息,请访问 bit.ly/24BSUC8

现在让我们来编写一个简单的页面,可显示的数据以及某些按钮的列表。在 Xamarin.Forms,页面是共享的元素,因此它们会添加到可移植的项目。为此,请在解决方案资源管理器中右键单击可移植的项目,然后选择添加 |新的项目。在添加新项对话框中,找到跨平台节点并选择该窗体 Xaml 页面模板,如中所示 图 13。命名新页 CustomersPage.cs 并单击添加。

添加一个新页基于 XAML
图 13,添加一个新页基于 XAML

为了显示客户列表,简单的用户界面将组成 ListView 控件的数据绑定到由 CustomersDataAccess 类公开客户集合。这些项的 DataTemplate 由四个输入控件,每个数据绑定到 Customer 模型类的属性组成。如果接触过 Windows Presentation Foundation (WPF) 和 UWP 等其他基于 XAML 的平台,可将条目视作,TextBox 控件的等效项。输入控件进行分组 StackLayout 面板,后者包含在 ViewCell 容器内。对于那些来自 WPF 和 UWP,StackLayout 是 StackPanel 容器的 Xamarin 等效项。ViewCell 允许创建自定义项控件 (如 ListView) 内的单元格。您会注意到,我对使用的输入控件 IsEnabled 属性指定为 to Customer.Id 属性,而不是一个标签控件,它是只读的性质,为 False。如您可能记得,当你调用 SQLiteConnection.Insert 方法时,这将更新映射为您的模型中的主键,因此 UI 应能自动反映此更改的属性。遗憾的是,Label 控件不会进行自我更新具有新值,而项控件的作用,并且这是条目被使用,但设置为只读的原因。

UI 的第二个部分包含 ToolbarItem 按钮,提供一种简单而共享的方式来提供通过将在所有平台上提供一个方便的菜单栏的用户交互。为简单起见,将在菜单栏中,不需要特定于平台的图标的辅助区域中实现这些按钮。图 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 出现在工具栏上,在已激活可以与一个 click 事件进行比较,以及要求事件处理程序的顺序。

下一步编写实例化 CustomersDataAccess 类的 C# 代码、 数据绑定对象,并对数据执行操作。图 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;
        }
      }
    }
  }
}

BindingContext 属性等同于在 WPF 和 UWP DataContext,表示当前页的数据源。

测试应用程序使用仿真程序

现在就可以测试该应用程序。在指定文件中,您需要更改的启动页。当创建 Xamarin.Forms 项目时,Visual Studio 2015 生成程序代码中编写和分配给名为 MainPage 的对象的页。此分配是在应用程序类构造函数,因此打开程序,并将应用程序构造函数,如下所示︰

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

您可能会感到不直接; 分配给 MainPage 的 CustomersPage 实例相反,它封装为 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 Portable) 和空白应用 (Xamarin.Forms 共享) 项目模板,基于 Pcl 和共享的项目,分别。通常情况下,在 Visual Studio 中可以共享代码中使用任一 Pcl,生成可重用.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 模式和公开到 UI 的命令,而不是处理单击事件,并可以考虑将工具栏项移动到菜单栏中提供特定于平台的图标中的主区域。从数据的角度来看,您可能需要使用关系和外键。由于处理都具有 SQLite 库并非易事,您可能需要考虑 SQLite Net 扩展库 (bit.ly/24yhhnP),您需要处理关系和更高级的方案简化了 C# 代码的开放源项目。这是只是可能的改进,则可以尝试的进一步研究的小列表。

总结

在许多情况下,移动应用程序需要本地数据存储。使用 SQLite,Xamarin.Forms 应用程序可以轻松地管理本地数据库使用开放源代码,则会为无服务器,并可移植的引擎,支持 C# 和 LINQ 查询。SQLite 提供非常直观的对象,用于处理表和数据库对象,使其很容易实现在任何平台上的本地数据访问。签出的 SQLite 文档 (sqlite.org/docs.html) 的详细信息和其他方案。


Alessandro Del Sole 后的一位 Microsoft MVP 2008。 他已经 5 次获得年度 MVP 这一殊荣,发表过很多关于 Visual Studio .NET 开发的书籍、电子书、指导视频和文章。Del Sole 担任解决方案开发人员专家对大脑 Sys (大脑 sys.it),将重点放在.NET 开发培训和咨询服务。你可以关注他的 Twitter @progalex

衷心感谢以下技术专家参与本文的审阅: Kevin Ashley 和 Sara Silva