65.9K
CodeProject 正在变化。 阅读更多。
Home

在 Android 上使用 Visual Studio 和 Xamarin 进行 SQLite 交互

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.54/5 (9投票s)

2017 年 2 月 17 日

CPOL

12分钟阅读

viewsIcon

18125

在本文中,我们将使用 Visual Studio 和 Xamarin(一个允许创建跨平台解决方案的开发框架)来开发一个简单的 Android 解决方案。

范围

该项目将使我们能够更广泛地分析 Android 解决方案的特点,例如项目文件的组织(及其结构)以及框架本身的特点,并讨论代码语法。我们将使用 C# 语言编写源代码。

项目定义

像往常一样,我们在 Visual Studio 中创建一个新项目。如果已安装 Xamarin 框架(此处不讨论其配置),则将提供 Android 模板。在本例中,我们选择一个空解决方案,以便稍后添加所需的类和对象。



具体来说,本文将介绍一个非常简单的项目:我们的目标是创建一个简单的应用程序,该应用程序能够接收一个文本框,允许输入字母数字字符串,并将它们保存在 SQLite 数据库中。此目标旨在提供足够的元素来进一步深入了解开发 Android 应用程序的更一般和基础的主题:项目结构、GUI 创建、通过代码进行控件以及——重要的是!——与数据库的交互(尽管可能很基本),这里使用一个可用的 ORM 作为 NuGet 包。

项目结构

在下图可以看到一个已完成的解决方案结构,以提供对 Android 项目总体组织的初步概览,以及每个文件夹的含义。



撇开定义解决方案 Activity 的类不谈,可以看到两个文件夹:AssetsResources。第一个包含您需要复制到设备的所有文件。在图中,我们看到其中有一个名为 test.db 的文件:这是我们的数据库,我们稍后将对其进行开发。在这里,我们只需要考虑,为了精确地与数据库交互,我们需要在将其物理移动到将运行该应用程序的智能手机/平板电脑/设备之前,将其视为解决方案文件。

Resources 文件夹的结构更加多样,包含多个子文件夹(例如,列表并非详尽无遗),用于定义字符串常量、图形对象、图标和程序中使用的图像。

在本例中,我们注意到 drawable 子文件夹(其中包含应用程序的图标文件 Icon.png)、layout 目录——包含 AXML 文件,这些文件使用 XML 语法定义应用程序屏幕及其上的控件,以及 values 目录,用于定义常量文件,例如通过引用应用程序的固定文本字符串(在 strings.xml 文件的情况下)将预定义标识符与它们关联。

在此情况下,列表还包括 C# 类 MainActivity.cs(将控制我们启动对象的类)和 SQLiteORM.cs,该类专门用于执行我们将要定义的基本数据库任务。

主布局

现在,让我们为应用程序的主屏幕定义布局。添加一个新项,然后选择 Android 布局类型。我们将此布局命名为 Main.axml



图形上,我们将创建三种类型的 LinearLayout 对象(容器,允许为内容保留一定屏幕空间,并为其设置方向),然后用所需的控件填充每个容器。在第一个 LinearLayout 中插入一个 EditText(可编辑文本框)和一个 Button(可点击对象);第二个将添加一个 ListView(我们将需要列出已添加的记录);第三个是一个 Button,用于执行可能的表的大规模清空操作。



将实现我们窗口的 XML 代码如下。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:p1="http://schemas.android.com/apk/res/android Jump "
    p1:orientation="vertical"
    p1:layout_width="match_parent"
    p1:layout_height="match_parent"
    p1:id="@+id/linearLayout1">
    <LinearLayout
        p1:orientation="horizontal"
        p1:layout_width="match_parent"
        p1:layout_height="wrap_content"
        p1:id="@+id/linearLayout2"
        p1:layout_gravity="fill_horizontal">
        <EditText
            p1:id="@+id/editText1"
            p1:layout_width="290dp"
            p1:layout_height="wrap_content"
            p1:layout_gravity="fill_horizontal" />
        <Button
            p1:text="Aggiungi"
            p1:layout_width="wrap_content"
            p1:layout_height="match_parent"
            p1:id="@+id/button1" />
    </LinearLayout>
    <LinearLayout
        p1:orientation="vertical"
        p1:layout_width="match_parent"
        p1:layout_height="match_parent"
        p1:id="@+id/linearLayout3"
        p1:layout_weight="75">
        <ListView
            p1:minWidth="25px"
            p1:minHeight="25px"
            p1:layout_width="match_parent"
            p1:layout_height="match_parent"
            p1:id="@+id/listView1" />
    </LinearLayout>
    <LinearLayout
        p1:orientation="horizontal"
        p1:minWidth="25px"
        p1:minHeight="25px"
        p1:layout_width="match_parent"
        p1:layout_height="wrap_content"
        p1:id="@+id/linearLayout4">
        <Button
            p1:text="Svuota"
            p1:id="@+id/button2"
            p1:layout_width="match_parent"
            p1:layout_height="match_parent"
            p1:layout_gravity="fill_horizontal" />
    </LinearLayout>
</LinearLayout>

在这里,您可以看到一些常见的参数,在此我们记下一些重要信息。首先,id 参数:这是一个基本设置,因为它定义了布局中控件的名称。没有它,您将无法从代码中引用该对象,因此,对于实际使用的所有控件,指向我/自定义它(形式为“@+id/name”)非常重要。layout_widthlayout_height 属性分别定义了控件的宽度和高度。您还可以通过一些保留字来指示值:例如,我们经常看到 match_parent 的使用,它表示属性应具有其容器的值,或者 wrap_content,或者它应该适应内容的变化;layout_weight 表示空间占用百分比。请注意 linearLayout3:为其指定了 75 的 layout_weight,或者明确表示希望为此控件获得 75% 的总空间。至于 LinearLayout,还有 orientation 属性,它表示控件的排列方式,即围绕一个图块排序,还是堆叠。对于具有 Text 属性的控件,它指定要显示在它们上的文本。

鼓励读者尝试这些和其他属性,以便熟悉它们。

主 Activity

布局被引用,然后由相应的 Activity(在具有操作代码的类中)使用。在本例中,我们定义一个新的 activity,我们将其命名为 MainActivity.cs



在其 OnCreate 事件中,它定义了 Activity 创建时运行的代码,我们可以指示使用哪个 AXML 文件,然后通过上面描述的 id 属性引用控件。请看以下示例。

public class MainActivity : Activity
{
    EditText  t = null;
    ListView lv = null;
 
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);
        SetContentView(Resource.Layout.Main);
 
        Button b1 = FindViewById<Button>(Resource.Id.button1);
        Button b2 = FindViewById<Button>(Resource.Id.button2);
 
        lv = FindViewById<ListView>(Resource.Id.listView1);
        t  = FindViewById<EditText>(Resource.Id.editText1);
 
        b1.Click += B1_Click;
        b2.Click += B2_Click;
 
        t.KeyPress += (object sender, View.KeyEventArgs e) => {
            e.Handled = false;
            if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter)
            {
                // TO DO
                e.Handled = true;
            }
        };
    }
}

通过 setContentView 函数,指定要使用的 AXML 文件,并指定包含在 Resource.Layout 类中的名称。由此我们理解构成项目的目录层次结构实际上是如何转换为类的,通过这些类,我们可以引用其中的元素。一旦完成,Resource.Id 类将公开在此布局中定义的所有 id。请注意,只需声明给定类的对象(以 Button b1 为例),并通过 findViewById 函数(经过适当的强制转换)将图形对象绑定到其变量。事实上,语句

Button b1 = FindViewById<Button>(Resource.Id.button1);

简单地说:我定义了一个名为 b1 的 Button 对象,它必须引用附加到此 Activity 的布局中 id 为 button1 的控件。

从现在开始,b1 对象将允许我们捕获使用该应用程序时所需的各种事件和属性数据。请注意,例如,如何声明 EditText t 上的 KeyPress 事件:通过 lambda 例程,您可以声明当文本框具有焦点时按下某个键时会发生什么(在这种情况下,只有按下 Enter 键时才会发生)。此外,您会注意到与 b1b2 的 Click 事件相关,可以通过简单地指示事件发生时要执行的例程来分配事件的句柄(例程将具有与事件调用者相同的签名,与 C# 语法一贯如此)。

数据库:实现和控制

如开头所述,此应用程序只存储文本字符串,因此数据库将非常简单。然而,实现原则不会改变,因此在此处进行分析很有用。很多时候,应用程序会为您提供创建数据库的任务(如果不存在)。其他时候,您可以创建一个默认数据库模板,并确保它被应用程序复制到适当的位置。我们将在本文中介绍第二种方法。

SQLite Expert Personal

一个非常有用的工具来创建 SQLite 类型数据库是 SQLite Expert Personal。您可以在此处下载:http://www.sqliteexpert.com/download.html Jump 

启动后,我们可以创建一个新数据库,并通过便捷的 GUI 定义构成它的表和字段。其用法非常直观,因此我们在此不再详述。



在本例中,我们将创建一个名为 test.db 的简单数据库,其中包含一个名为 mottos 的表,该表又包含一个名为 Motto 的 TEXT 类型字段。一旦保存在 PC 上,test.db 文件就可以像前面所示那样拖到 Assets 目录中:这样,它将被视为编译到应用程序中的二进制文件,并且我们将确保在设备上运行应用程序时复制它,如果操作系统条件需要(通常,如果它仍然不存在,或者其结构已更改)。

SQLite-net

现在,我们编写一个用于数据库控制的类,或者用于执行上述复制以及我们想要在应用程序开发中使用的说明。

我们将依赖一个开源且轻量级的库,即 SQLite-net。它可以作为 NuGet 包下载,也可以在 GitHub 上找到:https://github.com/praeclarum/sqlite-net Jump 

数据库管理类

让我们创建一个名为 SQLiteORM.cs 的新类。在对其结构进行更详细的介绍之后,下面是该类的完整列表。

using Android.App;
using SQLite;
using System.Collections.Generic;
using System.IO;
 
namespace AndroidTest01
{
    public class SQLiteORM
    {
        [Table("Motti")]
        public class Motti
        {
            public string Motto { get; set; }
        }
 
        public string PercorsoDb { get; set; }
 
        public SQLiteORM(string dbName)
        {
            string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
            if (!File.Exists(dbPath))
            {
                using (BinaryReader br = new BinaryReader(Application.Context.Assets.Open(dbName)))
                {
                    using (BinaryWriter bw = new BinaryWriter(new FileStream(dbPath, FileMode.Create)))
                    {
                        byte[] buffer = new byte[2048];
                        int len = 0;
                        while ((len = br.Read(buffer, 0, buffer.Length)) > 0)
                        {
                            bw.Write(buffer, 0, len);
                        }
                    }
                }
            }
 
            PercorsoDb = dbPath;
        }
 
        public List<string> GetMotto()
        {
            List<string> motti = new List<string>();
 
            using(var db = new SQLiteConnection(this.PercorsoDb))
            {
                foreach (var s in db.Table<Motti>())
                    motti.Add(s.Motto);
            }
 
            return motti;
        }
 
        public void InsertMotto(string motto)
        {
            using (var db = new SQLiteConnection(this.PercorsoDb))
            {
                db.Insert(new Motti { Motto = motto });
            }
        }
 
        public void Svuota()
        {
            using (var db = new SQLiteConnection(this.PercorsoDb))
            {
                db.DeleteAll<Motti>();
            }
        }
 
    }
}

其中,首先将其定义为一个子类,指示要交互的表名称和结构。

[Table("Motti")]
public class Motti
{
    public string Motto { get; set; }
}

[Table ("Motti")] 注释将标识数据库中的物理表名称,无论我们将分配给它的类名称是什么。出于实际目的,我们将保持它们相同。

请注意,GetMotto()InsertMotto()Clear 例程基本结构相同:每个例程都由 SQLiteConnection 类型对象的初始化介绍,该对象连接到指定数据库,然后执行例程的特定功能。

例如,在 GetMotto() 的情况下,即用于填充视图的例程,我们看到(在建立连接后)它会遍历 Table<Motti> 中的所有项目,并看到每个项目如何被添加到 List<string> 中,该列表将成为例程返回的类型。仍然,在 InsertMotto() 的情况下,它接受一个字符串作为必需的参数,建立连接后,将调用 insert 函数,传递一个 Motti 对象,其 Headline 属性是传递给方法的字符串。执行各种命令后,为了使用关键字,连接将被释放:换句话说,它会保持足够长的时间以执行命令。

现在,让我们关注构造函数。

public SQLiteORM(string dbName)
{
    string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
    if (!File.Exists(dbPath))
    {
        using (BinaryReader br = new BinaryReader(Application.Context.Assets.Open(dbName)))
        {
            using (BinaryWriter bw = new BinaryWriter(new FileStream(dbPath, FileMode.Create)))
            {
                byte[] buffer = new byte[2048];
                int len = 0;
                while ((len = br.Read(buffer, 0, buffer.Length)) > 0)
                {
                    bw.Write(buffer, 0, len);
                }
            }
        }
    }
 
    PercorsoDb = dbPath;
}

由于数据库在 Assets 目录中,并且我们希望将其复制到设备上,因此首先要检查它是否已经存在。我们在此假设您想将数据库保存在外部存储器上,通常是 SD 卡。然后,我们检查我们的数据库是否在该位置不存在。

string dbPath = Path.Combine(Android.OS.Environment.ExternalStorageDirectory.ToString(), dbName);
if (!File.Exists(dbPath))
{
    // TO DO
}

Android.OS.Environment.ExternalStorageDirectory 是一个用于读取存储卡物理位置的属性。然后,我们创建一个由该属性和我们的数据库名称组成的路径,然后验证其是否存在。如果不存在,我们将通过 BinaryReaderBinaryWriter 类来读取我们的数据库文件并执行复制。请注意,为了做到这一点,应用程序必须具有适当的权限。因此,我们来看看如何设置它们。

SD 卡的 I/O 权限

Android 操作系统要求,对于使用特殊功能的应用程序,它们必须通过清单文件声明其意图。必须指定应用程序正常运行所需的所有资源和行为。在本例中,如果我们不指定使用外部存储读写权限就运行应用程序,那么在尝试访问该资源时,应用程序本身就会崩溃,因为操作系统不会授予该权限。

指定此类权限很简单。我们进入项目属性,然后 Android Manifest 条目将描述该程序是特定的。在本例中,将在 READ_EXTERNAL_STORAGEWRITE_EXTERNAL_STORAGE 条目上打勾。

列表布局

尽管看起来我们现在已经拥有完成应用程序的所有元素,但实际上,有一个单独的段落专门介绍 ListView 控件,我们将使用它来显示数据库中存储的所有短语。像任何列表一样,它负责包含元素;然而,与 WinForms 上下文中的 ListView 不同,您无法直接访问 Items 属性,然后通过直接引用集合来突出显示元素。

即使从布局的角度来看,您也必须定义每个元素如何显示,在这方面,我们更接近 WPF 列表类型,但需要自定义 DataTemplate。换句话说,要正确使用 Android 的 ListView(它只是一个容器),我们首先为子元素定义一个布局,然后——通过适配器——将要显示的对象的集合与它们将被公开的图形模型一起列出。

这里呈现的案例尽可能简单(对于列表元素只有一个字符串),但如果这种方法可能显得过度,那么在组合非常复杂的图形时,它实际上非常灵活,在这种图形中,每个列表对象都必须以不同的形式提交更多数据。

然后,我们定义一个新的 AXML 布局。我们将其命名为 lvItem.axml,并在其中指定一个简单的 TextView 类型对象(相当于 WinForms 中的 Label)。因此,我们布局的 XML 代码将是:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:p1="http://schemas.android.com/apk/res/android Jump "
    p1:text="Medium Text"
    p1:textAppearance="?android:attr/textAppearanceMedium"
    p1:layout_width="match_parent"
    p1:layout_height="match_parent"
    p1:id="@+id/textView1" />

ListView 具有 Adapter 属性。因此,当使用 SQLiteORM.cs 类中定义的 GetMotto() 例程时,我们可以组合一个适配器,用于将数据集合和新创建布局所代表的资源发送给列表,从而启用显示。

现在,我们来看一下 MainActivity.cs 的完整代码,以便分析数据库操作以及将分配给每个控件的事件。

MainActivity.cs 完整代码

using System;
using Android.App;
using Android.Views;
using Android.Widget;
using Android.OS;
 
namespace AndroidTest01
{
    [Activity(Label = "AndroidTest01", MainLauncher = true, Icon = "@drawable/icon")]
    public class MainActivity : Activity
    {
        SQLiteORM o = null;
        EditText  t = null;
        ListView lv = null;
 
        protected override void OnCreate(Bundle bundle)
        {
            base.OnCreate(bundle);
            SetContentView(Resource.Layout.Main);
 
            Button b1 = FindViewById<Button>(Resource.Id.button1);
            Button b2 = FindViewById<Button>(Resource.Id.button2);
 
            lv = FindViewById<ListView>(Resource.Id.listView1);
            t  = FindViewById<EditText>(Resource.Id.editText1);
 
            o = new SQLiteORM("test.db");
            RefreshAdapter();
 
            b1.Click += B1_Click;
            b2.Click += B2_Click;
 
            t.KeyPress += (object sender, View.KeyEventArgs e) => {
                e.Handled = false;
                if (e.Event.Action == KeyEventActions.Down && e.KeyCode == Keycode.Enter)
                {
                    B1_Click(sender, e);
                    e.Handled = true;
                }
            };
        }
 
        private void RefreshAdapter()
        {
            lv.Adapter = new ArrayAdapter<string>(lv.Context, Resource.Layout.lvItem, o.GetMotto().ToArray());
        }
 
        private void B2_Click(object sender, EventArgs e)
        {
            AlertDialog.Builder alert = new AlertDialog.Builder(this);
            alert.SetTitle("Conferma eliminazione");
            alert.SetMessage("Verrà eliminato l'intero contenuto di tabella");
            alert.SetPositiveButton("Procedi", (senderAlert, args) => {
                o.Svuota();
                RefreshAdapter();
            });
 
            alert.SetNegativeButton("Annulla", (senderAlert, args) => {  });
 
            Dialog dialog = alert.Create();
            dialog.Show();
        }
 
        private void B1_Click(object sender, EventArgs e)
        {
            if (o != null)
            {
                if (t.Text != "")
                {
                    o.InsertMotto(t.Text);
                    RefreshAdapter();
                    t.Text = "";
                } else
                {
                    Toast.MakeText(this, "È necessario inserire una stringa", ToastLength.Short).Show();
                }
                t.RequestFocus();               
            }
        }
    }
}

与上面的代码相比,请注意 SQLiteORM 类型的变量声明,其构造函数将传递我们的数据库名称,以便应用程序能够验证其是否存在,或者在替代情况下,在外部存储上创建它。我们还注意到 RefreshAdapter 例程的存在:在其中,将 Adapter 属性分配给一个 ArrayAdapter 对象,该对象将转换为字符串。它通过传递适配器必须引用的上下文、用于单个元素图形设计的布局以及要显示的对象集合(在本例中是 GetMotto() 方法获得的字符串列表)来创建。

最后,分配给屏幕上按钮的 Click 事件的两个例程,B1_ClickB2_Click,分别执行在数据库中插入输入的字符串,或者在用户确认后清空数据库,通过对话框实现。在这两种情况下,操作完成后,调用 RefreshAdapter 以确保列表已使用实际数据重新填充。

演示

以下是一个视频,其中可以观察到由此创建的应用程序的快速演示(请参阅 URL:http://www.youtube.com/watch?v=rNAmeZD2Jzk  )。

下载

此处提供的源代码可从以下 URL 免费下载:https://code.msdn.microsoft.com/Interazione-con-SQLite-per-5150e933  

其他语言


本文也提供以下本地化版本。

© . All rights reserved.