通用移动数据采集控件
为 Windows Mobile 5 数据采集生成前端屏幕和数据库。

引言
大多数系统都需要一个手持设备来进行数据收集。这些设备在现场使用,收集数据后同步回服务器。数据可以通过链接线、互联网或Wi-Fi等方式同步。许多项目需要不同的数据收集方法,而为这些数据创建一个通用的收集屏幕对于开发人员来说是一项耗时的任务。
我的目标是创建一个通用的Windows窗体用户控件,可以放置在Windows Mobile 5-6项目中进行编译。编译后的版本然后在每个设备中使用,其界面链接到一个文本文件。
开发人员只需更改文本文件,数据库和界面就会生成供现场使用。同步过程在一个独立的应用程序中完成,这超出了本文的范围。
背景
我使用的是Windows Mobile 6 SDK (WM6) 和 Windows Mobile 6.5 DTK (请在VS2008之后安装)。
Visual Studio 2008 标准版以及WM6所需的其他更新。
SQL Compact 版。
注意: VS2010 不再支持 Windows Mobile 6。
我将开始在Visual Studio中创建一个移动项目。
然后我在项目中创建用户控件。
然后我创建另一个项目,它将是我的主要移动项目(一个解决方案中有两个项目)。
右键单击项目 -> 添加引用 -> 选择控件项目。
这样你就有了一个控件的引用。创建一个Windows窗体,你就可以在你编译的项目中使用用户控件了。
我将以最快的方式编写代码,使用位于移动设备上的一个文本文件:
/My Documents/Templates/test.txt.
你可以使用XML并解析XML,这比我目前使用的方法更好。
关于界面的另一个假设是,我的控件被分成多个标签页(Tabs)。每个标签页收集一组特定的数据,一个保存按钮将所有数据保存到一个表中。只有在同步过程中,我才会提取所需的数据。因此,如果一个标签页不需要填充数据,该数据的文本框可以留空。
这就是我解析文本文件的方式。我将整个文本分割成用“换行符”或“\n”和破折号“-”分隔的各个部分。然后我检查每个单词,并尝试找到以下内容的匹配项:
- 如果有一个小于等于号“<”,这个词就是标签页的名称。
- 如果有一个大括号“{”,这个词就是表中字段的名称。
- 任何其他词都是一个标签。
Using the Code
我将把所有代码放在“Controlname.Designer.cs”文件中。你可以根据需要更改控件的名称。右键单击文件并选择“查看代码”。
你将看到由设计器生成的以下代码。
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
你将看到一个名为“component designer generated code”的代码区域。大多数控件将在此方法中生成:
private void InitializeComponent() { ...}
在初始化控件之前,我首先需要一些方法来创建控件并将其返回给我。这样,我就可以调用一个方法,使用不同的控件ID生成多个控件。根据我对控件分组的假设,我需要以下控件:
- 文本框
- Label
- Button
- 标签页
- 标签控件(包含标签页)
当然,还有一个类型为ArrayList
的变量,这样我就知道我创建了多少个控件。
protected ArrayList myControls = new ArrayList();
然后我开始创建以下方法(根据标准添加你自己的附加方法):
private TabControl myTab(string ID)
{
TabControl tabc = new TabControl();
tabc.Name = "Tabc" + ID;
tabc.Size = new System.Drawing.Size(this.Width, this.Height);
tabc.SelectedIndex = 0;
return tabc;
}
private TabPage myTabPage(string ID, string txt, TabControl tabc)
{
TabPage tabp = new TabPage();
tabp.Name = "Tabp" + ID;
tabp.Text = txt;
tabc.Controls.Add(tabp);
return tabp;
}
private TextBox myTextbox(string ID,TabPage tabp, int myX, int myY)
{
TextBox txt = new TextBox();
txt.Name = "txt" + ID;
txt.Size = new System.Drawing.Size(100, 21);
txt.Location = new System.Drawing.Point(myX, myY);
tabp.Controls.Add(txt);
myControls.Add(txt);
return txt;
}
private Label myLabel(string ID, string txt, int myX, int myY)
{
Label lbl = new Label();
lbl.Name = "mLbl" + ID;
lbl.Size = new System.Drawing.Size(200, 21);
lbl.Text = txt;
lbl.Location = new System.Drawing.Point(myX, myY);
return lbl;
}
private Label myLabel(string ID, string txt, TabPage tabp, int myX, int myY)
{
Label lbl = new Label();
lbl.Name = "Lbl" + ID;
lbl.Size = new System.Drawing.Size(100, 21);
lbl.Text = txt;
tabp.Controls.Add(lbl);
lbl.Location = new System.Drawing.Point(myX, myY);
return lbl;
}
private Button myButton(string ID, string txt, int myX, int myY)
{
Button btn = new Button();
btn.Name = "Btn" + ID;
btn.Text = txt;
btn.Location = new System.Drawing.Point(myX, myY);
btn.Click += new System.EventHandler(this.btnSave_Click);
return btn;
}
创建完控件后,我需要一些通用的方法来检查连接、打开我的文本文件等等。
private string readFile(string filename)
{
string data = string.Empty;
try
{
System.IO.StreamReader sr =
new System.IO.StreamReader(@"\My Documents\Templates\"+filename);
data = sr.ReadToEnd();
sr.Close();
}
catch (Exception)
{
throw;
}
return data;
}
Read File 方法使用StreamReader
对象读取位于设备(移动设备)上的文本文件。如果此文件不存在,应用程序将在移动设备上加载时报错。
我的下一个方法将给出移动设备上数据库文件“.sdf”的位置。
private String DBFile()
{
return "/My Documents/Templates/myDBDataSet.sdf";
}
我还需要构建一个连接字符串并检查数据库文件是否存在。从下面的方法可以看出,我没有选择密码。你可以设置密码,以防你需要一个安全的数据库文件。CheckConnection()
方法确保在生成界面之前数据库文件存在。如果不存在,它将加载一个不同的界面。
private String ConnectionString()
{
return "Data Source ='"+ DBFile() +"'; Password ='';";
}
private bool CheckConnection()
{
SqlCeConnection localCon = new SqlCeConnection(ConnectionString());
if (!System.IO.File.Exists(DBFile()))
{
//MessageBox.Show("Database File Does not Exist");
return false;
}
else return true;
}
如果发生这种情况,我们没有数据库文件,或者因为更新了文本文件而删除了它,我们需要创建数据库。以下方法将在设备上为用户创建数据库(如果它不存在)。
private void CreateDB()
{
SqlCeEngine m = new SqlCeEngine(ConnectionString());
m.CreateDatabase();
MessageBox.Show("Database Created. Please Restart the Application");
}
另一个辅助方法是打开数据库连接并返回SqlCeConnection
。
private SqlCeConnection openCon()
{
try
{
SqlCeConnection con = new SqlCeConnection(ConnectionString());
con.Open();
return con;
}
catch (Exception) { return null; }
}
一切就绪,还有两个方法:一个用于在数据库中创建表,另一个用于将值INSERT
到表中。
private void CreateTable()
{
String myFields = "";
foreach (var f in fields)
{
myFields += ",[" + f.ToString() + "] [nvarchar](50) NULL ";
}
String tsql = String.Format
("CREATE TABLE [tblData]([ID] [int] IDENTITY(1,1) NOT NULL {0})",myFields);
using (SqlCeConnection con = openCon())
{
SqlCeCommand cmd = new SqlCeCommand(tsql, con);
cmd.CommandType = System.Data.CommandType.Text;
cmd.ExecuteNonQuery();
}
}
private void SaveData()
{
String myFields = "";
int j = fields.Count;
int i = 0;
foreach (var f in fields)
{
i++;
myFields += "[" + f.ToString() + "]";
if (i < j )
{
myFields += ",";
}
}
String myValues = "";
j = myControls.Count;
i = 0;
foreach (var o in myControls)
{
i++;
TextBox txt = o as TextBox;
myValues += "'" + txt.Text + "'";
if (i < j)
{
myValues += ",";
}
}
String tsql = String.Format
("INSERT INTO [tblData]({0}) VALUES ({1})", myFields, myValues);
using (SqlCeConnection con = openCon())
{
SqlCeCommand cmd = new SqlCeCommand(tsql, con);
cmd.CommandType = System.Data.CommandType.Text;
cmd.ExecuteNonQuery();
MessageBox.Show("Data Saved");
}
}
正如你所见,我使用了TSQL创建表,并分配了一个名为[id]
的标识列。你还会注意到我使用了一个名为“field
”的变量。这个变量是一个ArrayList
,其中保存着表的字段名。
private ArrayList fields = null;
我们在InitializeComponent
方法中填充这个变量的内容,这是下一个方法。
private void InitializeComponent()
{
this.SuspendLayout();
this.PerformAutoScale();
//this.AutoScaleDimensions = new System.Drawing.SizeF(96F, 96F);
//this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi;
int MaxX = this.Width = 160;
int MaxY = this.Height = 250;
int minX =5, minY = 0;
string myResult = readFile("App.txt");
char[] separator = (new Char[] { '-', '\n' });
string[] splotArr = myResult.Split(separator);
#region for INSERT/CREATE TABLE
fields = new ArrayList();
foreach (string s in splotArr)
{
if (s.Contains("{"))
{
String finalS = s.Replace("{", "");
finalS = finalS.Replace("}", "");
finalS = finalS.Replace("\r", "");
fields.Add(finalS);
}
}
#endregion
if (CheckConnection())
{
#region ReadFile
Random r = new Random();
//always create a tab
TabControl t = myTab("GenTab");
TabPage p = null;
foreach (string s in splotArr)
{
if (s.Contains("<"))
{
minX = 0; minY = 0;
String finalS = s.Replace("<", "");
finalS = finalS.Replace("\r", "");
finalS = finalS.Replace(">", "");
p = myTabPage(finalS, finalS, t);
}
else if (s.Contains("{"))
{
String finalS = s.Replace("{", "");
finalS = finalS.Replace("}", "");
finalS = finalS.Replace("\r", "");
minX = (MaxX / 2) + 30;
//s is a control textbox
myTextbox(finalS, p, minX, minY);
}
else
{
if (s.Trim() != "")
{
minX = 5;
minY += 25;
myLabel(r.Next().ToString(), s, p, minX, minY);
}
}
}
this.Controls.Add(t); //add tab
this.Controls.Add(myButton("bt", "Save", this.Width, this.Height + 20));
#endregion
}
else
{
this.Controls.Add(myLabel("ID1", "Database Does Not Exist.", 50, 50));
this.Controls.Add(myButton("bt", "Create", this.Width/2, this.Height/2));
}
this.Name = "Form";
this.ResumeLayout(false);
}
读取文本文件并解析后,你会注意到几点。首先,我检查数据库文件是否存在。如果不存在,我会在屏幕中央创建一个标签和一个创建按钮,告知用户没有数据库文件。因此,创建了一个新的文本文件,导致了以下屏幕:

用户点击创建按钮后,按钮处理程序包含以下内容:
private void btnSave_Click(object sender, EventArgs e)
{
Button btn = sender as Button;
switch (btn.Text)
{
case "Create": CreateDB(); CreateTable(); break;
case "Save": SaveData(); break;
}
}
创建按钮创建数据库文件和关联的表,并要求用户重启应用程序。一旦应用程序重启,initializecomponent
方法中的If
语句将返回true
,并将根据提到的标准生成屏幕,从而得到以下屏幕:

保存按钮通过保存处理程序将文本框中的数据保存起来,并向用户显示保存对话框。

关注点
你可以看到,更改文本文件需要用户手动删除数据库文件,以便应用程序根据文本文件生成新的数据库和表。你可以更改并改进这一点,创建一个删除表的方法,并检查旧表和新文本文件中的列。
需要注意的是,Windows Mobile 没有布局管理器,所以如果没有使用第三方控件,就无法创建流式布局或网格布局(如Java AWT)。尽管设计很基础,但它会根据某些标准(像素高度或宽度)来估算控件的位置。你可以创建自己的自定义布局管理器,并扩展它以实现更好的可视化。
历史
- 初稿 - Aresh Saharkhiz (2011年12月1日)