摘要:本文讲解怎样使用Pocket PC Phone Edition工具集迅速建立无线数据库应用程序。本文介绍的是使用Visual C# 和SQL Server CE 2.0建立一个Pocket PC Phone Edition应用程序。
新的移动计算时代开始了
移动解决方案系统体系结构的一部分正在迅速成为现实。最先是很多公司不断意识到移动解决方案可以带来效率的提高和新的商业机会。顾客正在每天的生活中开始看到移动设备的使用。移动设备硬件,特别是新的Pocket PC,正在引领企业和顾客需求的方向。把每种需求胶合在一起的关键就是新的软件和开发工具。
我使用新的Visual Studio .NET、Smart Device Extensions和新的SQL Server? CE 2.0工作了几个月时间。这些软件组件与连接的(connected)Pocket PC,例如Pocket PC Phone Edition,提高了开发效率并且已经驱动了很多新的移动应用程序。
SQL Server CE的新特性
总的来说,SQL Server CE反映了一种矛盾。作为移动设备的本地数据库,SQL Server CE支持不连接的环境。在大多数情况中,没有网络连接的Pocket PC上运行的Pocket PC应用程序需要本地数据存储。SQL Server CE也支持连接的环境,并且使数据向远程服务器或从远程服务器传递效率很高,无论是从开发还是从带宽来看。
下面是SQL Server CE的部分特性:
· 为本地的SQL Server CE管理和远程的SQL Server连接与.NET紧凑框架组件集成。
· 连接安装向导使SQL Server CE组件的安装简化。
· 大量的内部函数,包括很有价值的NEWID、CHAR、CHARINDEX、UNICODE、LEN、LTRIM、RTRIM、SPACE、 SUBSTRING、IDENT99vY、DATALENGTH等等。
· 联合(UNION,例如SELECT * FROM Orders UNION SELECT * FROM OldOrders)
· 使用远程数据访问牵引(Remote Data Access Pull)从远程服务器表索引得到数据的能力。
· 改良的SQL Server查询分析器(Query Analyzer)。
新的SQL Server CE 2.0数据访问结构依赖下面名字空间中的类:
· System.Data.SqlServerCE(使用合并复制和远程数据访问管理本地数据库和远程服务器连接)。
· System.Data.SqlClient(管理远程数据库并且包含对TSQL和存储过程的支持)。
随着数据访问结构转移到.NET紧凑框架组件,它里面的组件得到了改良,并且更容易使用了。例如,先前的合并复制初始化(Merge Replication Initialize)、运行和终止大纲(schema)被一个单独的方法System.Data.SQLServerCE.Replication.Synchronize所代替,它在第一次同步的时候建立大纲并下载数据,接着提交修改过的数据,并在接下来的同步中下载修改过的数据。
新的远程数据访问(Remote Data Access)类也有一定的改良,包含了从远程表索引取得数据的能力和为Push方法定义批处理模式(batch-mode)的能力。我将介绍一个"高尔夫得分卡"示例应用程序,它是使用SQL Server CE 2.0、 远程数据访问和Visual C#建立的。
示例应用程序:界面
示例应用程序Golf Anyplace可以运行在标准的Pocket PC上,但是在实际的高尔夫球场上Pocket PC Phone Edition的内建连接会有很大的好处。Golf Anyplace本质上是一个数字得分卡,它能跟踪你的得分和其它玩家的结果。其想法是每个打高尔夫的人使用Pocket PC Phone Edition跟踪得分。因为每个人都能把自己的分数发送到远程服务器并下载他人的得分,所以能够经常看到比赛的过程。
下面是一些界面:
主窗体用于输入自己的得分。它也可以用于查看其它玩家的得分。
图1.输入自己的得分
Synchronize命令把本地数据推入远程服务器,接着下载所有的远程得分数据。
图2.同步分数
你可以使用View窗体查看细节和整个比赛的情况。
图3.保持跟踪其它的玩家
示例应用程序:代码
下面我们看一看代码。在代码的某些位置,你可能注意到我使用不同的方法解决同一个问题。这些例子包括我怎样着手类的初始化,使用DataReader还是DataSet,填充ListView,是否使用SQL Server CE包装等等。我希望这能对你有些帮助,某个方案在某种情况下工作得很好,在其它的环境中可能另一个方案更好。
启动
Golf Anyplace的启动对象是GolfAnyplace.RDAGolf。下面是当启动应用程序时构造逻辑执行的操作:
public RDAGolf() { InitializeComponent(); //确定存在数据库 SQLServerCEWrapper SSCEWrapper = new SQLServerCEWrapper(); bool NewDatabase = SSCEWrapper.CreateDatabase(); //如果新数据库被建立了,执行第一次下载数据 if(NewDatabase==true) { //调用Pull并且不保存本地数据 SSCEWrapper.Pull(false); } //填充组合框 for(int iCounter=1; iCounter < 19; iCounter++) this.cmbHole.Items.Add(iCounter.ToString()); //把第一洞设置为默认的 this.cmbHole.SelectedIndex = 0; }
你可以看到我实现了SQL Server CE包装。我是通过把与数据库相关的代码写到同一个位置实现的。该包装帮助我管理并与本地数据库一起工作,以及远程数据库同步。下面是该包装的前面几行代码:
using System; using System.Data; using System.Windows.Forms; using System.Collections; using System.Data.Common; using System.Data.SqlServerCe; using System.Data.SqlClient; namespace GolfAnyplace { public class SQLServerCEWrapper { public string InternetServer = "http://servername/directory/sscesa20.dll"; public string InternetUser ="DOMAIN\\user"; public string InternetPassword = "password"; public string RemoteConnection = "Provider=sqloledb; Data Source=MySQLServer;Initial Catalog=GolfAnyplace;User Id=user;Password=password"; public string LocalDatabase = "\\My Documents\\ga.sdf"; public string LocalConnection = "Provider=Microsoft.SQLSERVER.OLEDB.CE.2.0; Data Source=\\My Documents\\ga.sdf"; public string LocalTableName = "Result"; public string RemoteTableName = "Result";
我使RDA属性共享的唯一原因是当我在应用程序的其它部分演示DataReader时需要它。实际上RDA属性应该是私有的,这样就能与应用程序的其它部分保持一致,并且可以自该包装传递一个DataSet。
下面是该包装的CreateDatabase和Pull方法:
public bool CreateDatabase() { // 确定数据库存在,如果新数据库被建立了就返回true,否则返回false。 if(System.IO.File.Exists(LocalDatabase) == false) { System.Data.SqlServerCe.Engine SQLCEEngine = new System.Data.SqlServerCe.Engine(LocalConnection); SQLCEEngine.CreateDatabase(); return true; } else { return false; }
如果数据库不存在,CreateDatabase方法就建立一个新数据库。
public void Pull(bool KeepLocalData) { //把表下载到本地数据库 string SQL; SqlCeConnection cn; SqlCeCommand cmd; RemoteDataAccess RDA = null; //建立和初始化新的RDA对象 RDA = new RemoteDataAccess(InternetServer, InternetUser, InternetPassword, LocalConnection); //保持本地数据码?如果保持,首先上载它。 if(KeepLocalData) { RDA.Push(LocalTableName, RemoteConnection, RdaBatchOption.BatchingOff); } //在下载前,必须删除本地表 //打开本地数据库连接 cn = new SqlCeConnection(LocalConnection); cn.Open(); //删除本地表 SQL = "DROP TABLE " + LocalTableName; cmd = new SqlCeCommand(SQL, cn); //如果表不存在,会出现错误 try { cmd.ExecuteNonQuery(); } catch{ } //关闭连接 cn.Dispose(); //最后下载远程表 SQL = "SELECT PlayerName, Hole, Result FROM " + RemoteTableName; RDA.Pull(LocalTableName, SQL, RemoteConnection, RdaTrackOption.TrackingOnWithIndexes, "RDAErrors"); //清除 RDA.Dispose(); }
注意我给Pull方法传递了一个布尔变量。如果本地已经存在某个表,那么把远程服务器表的内容传递到本地SQL Server CE表是无法做到的,因此传递前必须执行一个DROP TABLE语句。该布尔变量用于控制第一次向服务器传递数据时是否保持本地数据。你也可以看到Push方法的新的批处理参数。在本例中我使用了BatchingOff,这意味着我不认为提交到服务器的行是批处理的,而那意味着要么全部完成要么什么也不作。另一个可以的选项是BatchingOn。你可以看到,我使用了SqlCeCommand来执行DROP TABLE。
我想强调我在Pull语句中指定了字段的名称。记住通常要指定字段名称,无论是在Pull语句中还是在正常的SELECT语句中。通过这种做法,你可以确保只涉及绝对需要的数据。另外,你还可以轻易地防止与查找不能下载的字段(例如rowguidcol、int identity和timestamp字段)相关的问题。
服务器上有什么
在我们进一步深入该Pocket PC应用程序源代码前,我将解释以下服务器端有什么内容。远程服务器是SQL Server 2000,运行着一个叫GolfAnyplace的数据库,该数据库只有一个表Result。下面是该表的定义:
CREATE TABLE [dbo].[Result] ( [PlayerName] [nvarchar] (50) NOT NULL , [Hole] [smallint] NOT NULL , [Result] [smallint] NOT NULL ) ON [PRIMARY] GO ALTER TABLE [dbo].[Result] W99vH NOCHECK ADD CONSTRAINT [PK_Result] PRIMARY KEY CLUSTERED ( [PlayerName], [Hole], [Result] ) ON [PRIMARY]
我使用帮助向导配置了SQL Server CE 2.0服务器代理。
填充ListView
GolfAnyplace项目演示了两种填充listview的方法:使用DataSet和使用DataReader。因为我选择在SQL Server CE周围实现包装,所以最好使用DataSet。DataSet可以不连接,并且在不连接状态下在类之间传递。因为DataReader要求打开到数据库的连接,因此尽可能在打开和关闭连接附近使用它。第一个例子代码显示了怎样通过在DataSet循环填充一个listview。
下面是填充listview的代码:
private void UpdateResultListView() { //更新listview DataSet ds = null; SQLServerCEWrapper SSCEWrapper = new SQLServerCEWrapper(); //得到DataSet ds = SSCEWrapper.GetPlayerResult(txtPlayerName.Text); //清除listview lvwResult.Items.Clear(); //在DataSet中循环 foreach (DataRow dr in ds.Tables[0].Rows) { ListViewItem lviItem = new ListViewItem(dr["Hole"].ToString()); lviItem.SubItems.Add(dr["Result"].ToString()); lvwResult.Items.Add(lviItem); } }
下面是在包装类中与之对应的代码:
public DataSet GetPlayerResult(string PlayerName) { //从本地数据库得到数据,作为DataSet返回 string SQL; SqlCeConnection cn; SqlCeCommand cmd; DataSet ds; SqlCeDataAdapter da; //初始化新的连接 cn = new SqlCeConnection(LocalConnection); //打开连接 cn.Open(); //建立SQL表达式 SQL = "SELECT PlayerName, Hole, Result FROM " + LocalTableName + " WHERE PlayerName = '" + PlayerName + "' ORDER BY Hole"; //初始化新的命令 cmd = new SqlCeCommand(SQL,cn); //初始化新的DataSet ds = new DataSet(); da = new SqlCeDataAdapter(SQL,cn); //使用数据适配器用数据填充DataSet da.Fill(ds, LocalTableName); //清除 cn.Dispose(); cmd.Dispose(true); // Return the DataSet return ds; }
下面的示例代码演示了怎样使用DataReader作相同的事情:
private void UpdateDetailedResultListView() { //使用DataReader更新详细结果 SQLServerCEWrapper SSCEWrapper = new SQLServerCEWrapper(); string SQL; //详细listview的SQL表达式 SQL = "SELECT PlayerName, Hole, Result FROM Result ORDER BY PlayerName, Hole"; SqlCeConnection cn = new SqlCeConnection(); cn.ConnectionString = SSCEWrapper.LocalConnection; //打开连接 cn.Open(); //初始化命令并执行阅读程序 SqlCeCommand cmd = new SqlCeCommand(SQL,cn); SqlCeDataReader dtr = cmd.ExecuteReader(); //清除listview this.lvwResultDetailed.Items.Clear(); //开始读取 while (dtr.Read()) { ListViewItem lviItem = new ListViewItem(dtr["PlayerName"].ToString()); lviItem.SubItems.Add(dtr["Hole"].ToString()); lviItem.SubItems.Add(dtr["Result"].ToString()); lvwResultDetailed.Items.Add(lviItem); } //关闭DataReader dtr.Close(); //清除 cmd.Dispose(true); cn.Dispose(); }
最后,注意我在项目中使用了一个Options窗体(图4),需要一些附加的代码来支持它。
图4. SQL Server CE Options窗体
技巧
下面是一些我希望与你共享的技巧:
· 很好地设计界面。
· 不能让SQL Server CE代码没有Try、Catch和Finally等适当的错误处理程序。
· 如果你没有Pocket PC用于开发或者Pocket PC没有网卡,可以使用Pocket模拟器。
· 信任SQL Server CE连接向导,至少开始时信任。
· 当上载和下载数据时使用服务器端SQL Profiler或者合并复制监视发生的情况。如果没有其它情况发生,你可以看到什么时候或是否发生了某事。
· 学习在Internet上在何处查找编码问题的答案。
结论
Visual Studio .NET、Visual C#或Visual Basic .NET与SQL Server CE 2.0一起工作时匹配得很好。希望你能从本文我提供的方法中学习到一些好的想法。