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






4.54/5 (9投票s)
在本文中,我们将使用 Visual Studio 和 Xamarin(一个允许创建跨平台解决方案的开发框架)来开发一个简单的 Android 解决方案。
范围
该项目将使我们能够更广泛地分析 Android 解决方案的特点,例如项目文件的组织(及其结构)以及框架本身的特点,并讨论代码语法。我们将使用 C# 语言编写源代码。
项目定义
像往常一样,我们在 Visual Studio 中创建一个新项目。如果已安装 Xamarin 框架(此处不讨论其配置),则将提供 Android 模板。在本例中,我们选择一个空解决方案,以便稍后添加所需的类和对象。
具体来说,本文将介绍一个非常简单的项目:我们的目标是创建一个简单的应用程序,该应用程序能够接收一个文本框,允许输入字母数字字符串,并将它们保存在 SQLite 数据库中。此目标旨在提供足够的元素来进一步深入了解开发 Android 应用程序的更一般和基础的主题:项目结构、GUI 创建、通过代码进行控件以及——重要的是!——与数据库的交互(尽管可能很基本),这里使用一个可用的 ORM 作为 NuGet 包。
项目结构
在下图可以看到一个已完成的解决方案结构,以提供对 Android 项目总体组织的初步概览,以及每个文件夹的含义。
撇开定义解决方案 Activity 的类不谈,可以看到两个文件夹:Assets 和 Resources。第一个包含您需要复制到设备的所有文件。在图中,我们看到其中有一个名为 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_width
和 layout_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 键时才会发生)。此外,您会注意到与 b1
和 b2
的 Click 事件相关,可以通过简单地指示事件发生时要执行的例程来分配事件的句柄(例程将具有与事件调用者相同的签名,与 C# 语法一贯如此)。
数据库:实现和控制
如开头所述,此应用程序只存储文本字符串,因此数据库将非常简单。然而,实现原则不会改变,因此在此处进行分析很有用。很多时候,应用程序会为您提供创建数据库的任务(如果不存在)。其他时候,您可以创建一个默认数据库模板,并确保它被应用程序复制到适当的位置。我们将在本文中介绍第二种方法。
SQLite Expert Personal
一个非常有用的工具来创建 SQLite 类型数据库是 SQLite Expert Personal。您可以在此处下载:http://www.sqliteexpert.com/download.html 。
启动后,我们可以创建一个新数据库,并通过便捷的 GUI 定义构成它的表和字段。其用法非常直观,因此我们在此不再详述。
在本例中,我们将创建一个名为 test.db 的简单数据库,其中包含一个名为 mottos 的表,该表又包含一个名为 Motto 的 TEXT 类型字段。一旦保存在 PC 上,test.db 文件就可以像前面所示那样拖到 Assets 目录中:这样,它将被视为编译到应用程序中的二进制文件,并且我们将确保在设备上运行应用程序时复制它,如果操作系统条件需要(通常,如果它仍然不存在,或者其结构已更改)。
SQLite-net
现在,我们编写一个用于数据库控制的类,或者用于执行上述复制以及我们想要在应用程序开发中使用的说明。
我们将依赖一个开源且轻量级的库,即 SQLite-net。它可以作为 NuGet 包下载,也可以在 GitHub 上找到:https://github.com/praeclarum/sqlite-net 。
数据库管理类
让我们创建一个名为 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
是一个用于读取存储卡物理位置的属性。然后,我们创建一个由该属性和我们的数据库名称组成的路径,然后验证其是否存在。如果不存在,我们将通过 BinaryReader
和 BinaryWriter
类来读取我们的数据库文件并执行复制。请注意,为了做到这一点,应用程序必须具有适当的权限。因此,我们来看看如何设置它们。
SD 卡的 I/O 权限
Android 操作系统要求,对于使用特殊功能的应用程序,它们必须通过清单文件声明其意图。必须指定应用程序正常运行所需的所有资源和行为。在本例中,如果我们不指定使用外部存储读写权限就运行应用程序,那么在尝试访问该资源时,应用程序本身就会崩溃,因为操作系统不会授予该权限。
指定此类权限很简单。我们进入项目属性,然后 Android Manifest 条目将描述该程序是特定的。在本例中,将在 READ_EXTERNAL_STORAGE
和 WRITE_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_Click
和 B2_Click
,分别执行在数据库中插入输入的字符串,或者在用户确认后清空数据库,通过对话框实现。在这两种情况下,操作完成后,调用 RefreshAdapter 以确保列表已使用实际数据重新填充。
演示
以下是一个视频,其中可以观察到由此创建的应用程序的快速演示(请参阅 URL:http://www.youtube.com/watch?v=rNAmeZD2Jzk )。
下载
此处提供的源代码可从以下 URL 免费下载:https://code.msdn.microsoft.com/Interazione-con-SQLite-per-5150e933
其他语言
本文也提供以下本地化版本。