Trong bài viết trước về Xamarin.Forms, chúng ta đã tìm hiểu về cách thức kết buộc dữ liệu đến giao diện ứng dụng Xamarin.Forms. Trong bài viết này, chúng ta sẽ tìm hiểu cách thức một ứng dụng Xamarin.Forms tương tác với cơ sở dữ liệu từ hệ quản trị SQLite và chúng ta sẽ hiểu rõ hơn về cách thức kết buộc dữ liệu trong Xamarin.Forms.

Tạo ứng dụng Xamarin.Forms sử dụng Visual Studio 2017 Community

Tạo ra một ứng dụng Xamarin.Forms mới bằng cách vào File > New > Project. Trong cửa sổ New Project,  tại mục Visual C# ở khung bên trái chọn Cross-Platform, chọn Mobile App (Xamarin.Forms) ở khung giữa và gõ tên ứng dụng là SQLiteXamarinDemo trong ô Name cũng như chọn vị trí lưu ứng dụng trong ô Location:

Nhấn OK đóng hộp thoại New Project sẽ xuất hiện hộp thoại New Cross Platform App

Mặc định, chúng ta có thể thực thi ứng dụng Xamarin.Forms và chia sẻ mã qua lại giữa các nền tảng Android, iOS và Windows (UWP). Chọn Blank App và để giới hạn ứng dụng trên nền tảng Android cũng như để tránh dự án trở nên cồng kềnh, chúng ta chỉ chọn Android tại mục Platform và .NET Standard tại Code Sharing Strategy (tham khảo Code Sharing Strategy tại https://docs.microsoft.com/en-us/xamarin/cross-platform/app-fundamentals/code-sharing )

Nhấn OK. Như vậy chúng ta đã hoàn thành việc tạo một ứng dụng Xamarin.Forms. Như vậy lúc này chúng ta có hai dự án trong cửa sổ Solution Explorer:

Kế tiếp là cài gói thư viện SQLite đến Visual Studio.

Cài gói thư viện SQLite (SQLite Nuget Package)

Để sử dụng hệ quản trị SQLite trong môi trường Visual Studio (ở đây là bản 2017 Community), chúng ta cần cài gói thư viện SQLite tên sqlite-net-pcl. Để cài thư viện này, chúng ta vào mục Tools chọn NuGet Package Manager > Manage NuGet Packages for Solution:

Trong tab NuGet-Solution, chọn mục Browse và gõ cụm từ “pcl sqlite” trong ô tìm kiếm và chọn sqlite-net-pcl

Chúng ta cũng cần cẩn thận tại bước chọn thư viện này vì có thể chọn SQLite.Net-PCL thay vì sqlite-net-pcl

Đây là hai gói thư viện hoàn toàn khác biệt.

Kế tiếp chọn Project trong ô Version(s) và nhấn Install

Nhấn OK trong hộp Preview Changes (nếu xuất hiện). Chờ một vài giây, nếu cài đặt thành công thì trong cửa sổ Output sẽ trông như sau:

Giống như làm việc với bất kỳ cơ sở dữ liệu nào, để kết nối đến cơ sở dữ liệu SQLite chúng ta cần sử dụng chuỗi kết nối.

Chuỗi kết nối

Mặc dù ứng dụng chúng ta chỉ làm việc trên Android nhưng để chuỗi kết nối có thể sử dụng trên nhiều platform khác nhau, chúng ta sẽ tạo ra một interface trong dự án SQLiteXamarinDemo bằng cách thêm lớp IDatabaseConnection đến dự án này (cách nhấn chuột phải vào tên dự án SQLiteXamarinDemo trong cửa sổ Solution Explorer chọn Add > Class):


namespace SQLiteXamarinDemo

{

   public interface IDatabaseConnection

   {

     SQLite.SQLiteConnection DbConnection();

   }

}

Kế tiếp là tạo chuỗi kết nối trên nền tảng Android bằng cách nhấn chuột phải vào tên dự án SQLiteXamarinDemo.Android trong cửa sổ Solution Explorer chọn Add > Class

Gõ tên MyConnection trong mục Name và nhấn Add. Lớp MyConnection mặc định như sau:


using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using Android.App;

using Android.Content;

using Android.OS;

using Android.Runtime;

using Android.Views;

using Android.Widget;

namespace SQLiteXamarinDemo.Droid

{

  class MyConnection

  {

  }

}

Thay đổi nội dung tập tin MyConnection.cs như sau:


using SQLite;

using System.IO;

using SQLiteXamarinDemo.Droid;

[assembly: Xamarin.Forms.Dependency(typeof(MyConnection))]

namespace SQLiteXamarinDemo.Droid

{

  class MyConnection : IDatabaseConnection

  {

    public SQLiteConnection DbConnection()

    {

     var dbName = "ProductsDB.db3";

     var path = Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.Personal), dbName);

     return new SQLiteConnection(path);

    }  

  }

}

Lớp MyConnection thực thi giao diện IDatabaseConnection, phương thức DbConnection trả về đối tượng kiểu SQLiteConnection với chuỗi kết nối chứa thông tin cơ sở dữ liệu. Thuộc tính Xamarin.Forms.Dependency để xác định lớp thực thi giao diện (lớp MyConnection). Tập tin cơ sở dữ liệu của chúng ta là MyProductsDatabase.db3 và vì trong Android, tập tin cơ sở dữ liệu được lưu trong thư mục Personal, nên đường dẫn phải bao gồm tên tập tin cơ sở dữ liệu và thư mục Personal (path). Cuối cùng, biến đường dẫn được gán như tham số của phương thức khởi tạo lớp SQLiteConnection trả về từ phương thức DbConnection.

Khi chuỗi kết nối đã sẵn sàng, bước tiếp theo là xây dựng các lớp mô hình dữ liệu và lớp xử lý trước khi tạo giao diện người dùng. Trước khi bắt đầu xây dựng các lớp dữ liệu, chúng ta xem lại mô hình ứng dụng của chúng ta như sau:

Lớp Product phản ánh các thông tin từ bảng Product trong cơ sở dữ liệu ProductsDB; lớp DataHandler chứa các phương thức xử lý các dữ liệu nhận từ giao diện người dùng (User Interface) như thêm, xóa, cập nhật dữ liệu đến bảng Product.

Lớp mô hình dữ liệu

Chúng ta thêm lớp tên Product đến dự án bằng cách nhấn chuột phải vào dự án SQLiteXamarinDemo (không phải SQLiteXamarinDemo.Android) trong cửa sổ Solution Explorer chọn Add > Class, chọn Class và gõ Product trong Name:

Nhấn Add. Lớp Product trong tập tin Product.cs được thay đổi lại như sau:


using System.ComponentModel;

using SQLite;

namespace SQLiteXamarinDemo

{

  [Table("Product")]

  class Product

  {

    private int _id;

    [PrimaryKey]

    public int Id

    {

      get

       {

        return _id;

       }

      set

       {

        this._id = value;

       }

   }

   private string _productName;

   [NotNull]

   public string ProductName

   {

     get

     {

      return _productName;

     }

     set

    {

     this._productName = value;

    }

   }

}

}

Ở đây chúng ta định nghĩa lớp Product với hai biến thành viên là _id và _productName. Các biến thành viên này được ánh xạ đến các cột tương ứng trong bảng Product của cơ sở dữ liệu thông qua các thuộc tính đặc biệt trong namespace SQLite như [Table], [PrimaryKey], [NotNull]. Các thuộc tính có quy tắc ràng buộc giống các thuộc tính từ namespace System.ComponentModel.DataAnnotations.

Lớp xử lý dữ liệu

Giả sử ứng dụng của chúng ta chứa các chức năng như thêm một product mới, xóa một hay tất cả các products trong database, tìm kiếm một product và cập nhật thông tin về một product bất kỳ. Các chức năng trên tương ứng với các phương thức trong lớp xử lý dữ liệu tên DataHandler (trong mô hình ứng dụng ở trên của chúng ta). Thêm lớp DataHandler đến dự án SQLiteXamarinDemo. Một vài nội dung đầu tiên chúng ta thêm đến lớp DataHandler như sau:


class DataHandler

{

  private SQLiteConnection database;

  private static object collisionLock = new object();

  public ObservableCollection<Product> Products { get; set; }

  public DataHandler()

  {

   database =

        DependencyService.Get<IDatabaseConnection>().DbConnection();

   database.CreateTable<Product>();

   this.Products = new ObservableCollection<Product>(database.Table<Product>());

   if (!database.Table<Product>().Any())

   {

     AddNewProduct();

   }

  }

  public void AddNewProduct()

  {

    Product p = new Product()

    {

     Id = 0,

     ProductName = "Product name..."

    };

    this.Products.Add(p);

  }

}

Hai thành viên đầu tiên là biến database dùng để chứa chuỗi kết nối và biến collisionLock kiểu object đóng vai trò bảo vệ thao tác dữ liệu khi thao tác đó đang thực thi, tránh sự can thiệp từ các thao tác khác gây xung đột cơ sở dữ liệu.

Thuộc tính Products kiểu ObservableCollection được khai báo để thuận tiện cho quá trình kết nối dữ liệu đến giao diện XAML của Xamarin.Forms.

Phương thức khởi tạo của lớp DataHandler sử dụng phương thức DependencyService.Get để gọi phương thức DbConnection() của một platform cụ thể thông qua giao diện IDatabaseConnection. Phương thức CreateTable của đối tượng SQLiteConnection được gọi để tạo một bảng với kiểu lớp Product. Thuộc tính Products cũng được khởi tạo bằng cách gọi phương thức Table của đối tượng SQLiteConnection. Phương thức này trả về một danh sách các đối tượng kiểu Product và có thể áp dụng LINQ để truy vấn. Để tiện làm việc với giao diện XAML, một đối tượng ObserableCollection<Product> được phát sinh và gán đến thuộc tính Products.

Bảng mới được kiểm tra có rỗng hay không dùng phương thức truy vấn LINQ any(), nếu bảng rỗng, một hàng mới sẽ được thêm bằng cách gọi phương thức AddNewProduct().

Trước khi tiếp tục với các phương thức khác trong lớp DataHandler, chúng ta sẽ tạm dừng và chuyển sang thiết kế giao diện XAML của ứng dụng chúng ta.

Giao diện người dùng

Thêm một giao diện Xamarin.Forms mới đến dự án SQLiteXamarinDemo bằng cách nhấn chuột phải vào SQLiteXamarinDemo trong cửa sổ Solution Explorer, chọn Add > New Item, chọn Xamarin.Forms và Content Page và nhập ProductsPage trong ô Name và nhấn Add.

Nội dung mặc định của tập tin ProductsPage.xaml sẽ như sau:


<?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="SQLiteXamarinDemo.ProductsPage">

   <ContentPage.Content>

     <StackLayout>

       <Label Text="Welcome to Xamarin.Forms!"

            VerticalOptions="CenterAndExpand"

            HorizontalOptions="CenterAndExpand" />

     </StackLayout>

    </ContentPage.Content>

</ContentPage>

Xóa Label trong StackLayout và thay thế bằng nội dung sau:


<ListView x:Name="ProductsView"

      ItemsSource="{Binding Path=Products}"

      ListView.RowHeight="150">

    <ListView.ItemTemplate>

       <DataTemplate>

         <ViewCell>

           <StackLayout Orientation="Vertical">

              <Entry Text="{Binding Id}"/>

              <Entry Text="{Binding ProductName}" />

          </StackLayout>

         </ViewCell>

        </DataTemplate>

      </ListView.ItemTemplate>

</ListView>

<Button Text="Add" Clicked="OnAddClick"/>

Danh sách các product sẽ chứa trong một ListView và được nhóm theo các Entry Id và ProductName trong ViewCell. Dưới ListView là một Button với hàm xử lý sự kiện Click được định nghĩa trong tập tin ProductsPage.xaml.cs (còn gọi là CodeBehind):

Nội dung mặc định của tập tin ProductsPage.xaml.cs như sau:


using Xamarin.Forms;

using Xamarin.Forms.Xaml;

namespace SQLiteXamarinDemo

{

   [XamlCompilation(XamlCompilationOptions.Compile)]

   public partial class ProductsPage : ContentPage

   {

      public ProductsPage ()

      {

        InitializeComponent ();

       }

   }

}

Nội dung trong lớp ProductsPage được thay đổi như sau:


private DataHandler dataAccess;

public ProductsPage ()

{

  InitializeComponent ();

  this.dataAccess = new DataHandler();

}

protected override void OnAppearing()

{

  base.OnAppearing();
 
  this.BindingContext = this.dataAccess;

}

private void OnAddClick(object sender, EventArgs e)

{

  this.dataAccess.AddNewProduct();

}

Biến kiểu DataHandler được thêm để truy cập đến các phương thức chức năng từ lớp này, ví dụ AddNewProduct. Hàm OnAppearing thực thi khi trang Xamarin.Forms xuất hiện (giống sự kiện Load của Windows Forms). Khi trang ProductsPage xuất hiện, đối tượng lớp DataHandler sẽ được kết buộc đến trang. Hàm xử lý sự kiện Click của Button Add đơn giản chỉ gọi phương thức AddNewProduct từ đối tượng DataHandler.

Trước khi thực thi ứng dụng chúng ta cần xác định trang ProductsPage sẽ là trang được hiển thị khi ứng dụng chạy bằng cách vào  tập tin App.xaml.cs:

Khởi tạo lại MainPage trong hàm khởi tạo:


public App ()

{

  InitializeComponent();

   //MainPage = new SQLiteXamarinDemo.MainPage();

   MainPage = new NavigationPage(new ProductsPage());

}

Lúc này chúng ta có thể thực thi ứng dụng. Chúng ta có thể gặp lỗi sau:

Lỗi này có thể tìm hiểu tại https://stackoverflow.com/questions/51885218/cant-build-hello-world-xamarin-app-for-android. Giải pháp cho ứng dụng của chúng ta là tìm đến tập tin SQLiteXamarinDemo.Android.csproj và thay đổi nội dung trong <PropertyGroup>:


<?xml version="1.0" encoding="utf-8"?>

<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

   <PropertyGroup>

      <UseShortFileNames>True</UseShortFileNames>

      <IntermediateOutputPath>D:\Temp</IntermediateOutputPath>

   </PropertyGroup>

</Project>

Lưu ý, trong trường hợp này chúng ta phải tạo sẵn một thư mục trống Temp trong ổ D:\. Biên dịch lại chương trình và thực thi trên máy ảo mô phỏng thiết bị Android (bài viết này sử dụng Genymotion):

Có thể gõ tên sản phẩm đầu tiên (ví dụ Mac) trong mục Product Name và Id (Id mặc định là 0). Nếu muốn thêm sản phẩm khác nhấn nút Add:

Lưu ý rằng, đây là danh mục các sản phẩm chờ được duyệt để lưu vào cơ sở dữ liệu. Muốn lưu tất cả sản phẩm hay một vài sản phẩm nào đó đang chờ thì chúng ta cần bổ sung thêm hai tính năng lưu tất cả (SaveAllProducts) và lưu một sản phẩm nào đó (SaveProduct) đến lớp DataHandler như sau:


public void SaveProduct(Product p)

{

  lock (collisionLock)

  {

    database.Insert(p);

  }


}

public void SaveAllProducts()

{

  foreach (var p in this.Products)

  {

    lock (collisionLock)

    {

      database.Insert(p);

    }

  }

}

Trong tập tin ProductsPage.xaml chúng ta cũng bổ sung hai Button là Save và Save All:
<Button Text=”Save” Clicked=”OnSaveClick”/>


<Button Text="Save All" Clicked="OnSaveAllClick"/>

Hai hàm OnSaveClick và OnSaveAllClick được định nghĩa trong tập tin ProductsPage.xaml.cs:


private void OnSaveClick(object sender, EventArgs e)

{

  var p = this.ProductsView.SelectedItem as Product;

  if(p!= null)

     this.dataAccess.SaveProduct(p);

}

private void OnSaveAllClick(object sender, EventArgs e)

{

  this.dataAccess.SaveAllProducts();

}

Lưu tất cả và thực thi lại ứng dụng. Dùng nút Add để thêm hai sản phẩm chờ duyệt:

Nếu nhấn Save All, chúng ta sẽ lưu cả hai sản phẩm vào cơ sở dữ liệu. Giả sử chúng ta chỉ muốn lưu sản phẩm iPhone thì chỉ việc chọn sản phẩm này và nhấn nút Save:

Để kiểm tra sản phẩm có được lưu hay không hãy tắt ứng dụng và thực thi lại, kết quả:

Chúng ta đã thiết kế chức năng thêm sản phẩm chờ duyệt và lưu các sản phẩm này đến cơ sở dữ liệu. Để kết thúc bài viết này chúng ta sẽ thêm tính năng xóa một (DeleteProduct) sản phẩm và xóa tất cả sản phẩm (DeleteAllProducts) trong cơ sở dữ liệu đến lớp DataHandler:


public void DeleteProduct(Product p)

{

  var id = p.Id;

  if (id != 0)

  {

     lock (collisionLock)

    {

      database.Delete<Product>(id);

    }

}

this.Products.Remove(p);

}

public void DeleteAllProducts()

{

  lock (collisionLock)

  {

    database.DropTable<Product>();

    database.CreateTable<Product>();

  }

  this.Products.Clear();

  this.Products = new ObservableCollection<Product>(database.Table<Product>());

}

Thêm hai Button đến tập tin ProductsPage.xaml:


<Button Text="Delete" Clicked="OnRemoveClick"/>

<Button Text="Delete All" Clicked="OnRemoveAllClick"/>

Hai hàm OnRemoveClick và OnRemoveAllClick được định nghĩa trong tập tin ProductsPage.xaml.cs:


private void OnRemoveClick(object sender, EventArgs e)

{

  var p = this.ProductsView.SelectedItem as Product;

  if (p != null)

     this.dataAccess.DeleteProduct(p);

}

private async void OnRemoveAllClick(object sender, EventArgs e)

{

   if (this.dataAccess.Products.Any())

    {

      var result =

        await DisplayAlert("Confirmation",

           "Are you sure? This cannot be undone",

           "OK", "Cancel");

      if (result == true)

      {

       this.dataAccess.DeleteAllProducts();

       this.dataAccess = new DataHandler();

       this.BindingContext = this.dataAccess;

      }

    }

}

Lưu và thực thi ứng dụng. Lần trước chúng ta đã lưu sản phẩm iPhone nên giao diện sẽ như thế này:

Chọn sản phẩm iPhone và nhấn nút Delete

Bây giờ nếu khởi động lại ứng dụng:

Lời kết

Trong bài viết này chúng ta đã tìm hiểu về cách thức một ứng dụng Xamarin.Forms làm việc với hệ quản trị cơ sở dữ liệu SQLite. Các chức năng như thêm, xóa, lưu chỉ được viết một cách đơn giản để minh họa. Mã nguồn dự án trong bài viết này có thể tải tại https://github.com/TranNgocMinh/SQLiteXamarinFormsDemo.