下载本文的代码:DataPoints0309.exe (150KB)
手持设备和其他便携式设备所附带的功能不断增加。现在,这些设备可以利用 Wi-Fi 和手机内置的无线调制解调器,通过专用网络和 Internet 进行通信。手持设备并不仅限于在个人用户中使用,许多商业领域也都在利用它们所提供的移动功能。人们希望便携功能更加完善,而这种需求的增加意味着对移动应用程序需求也在增加。举例来说,需要应用程序能让员工从字段更新商品目录、输入订单、更新顾客信息以及执行其他以数据为中心的任务。
在本专栏中,我将开发一个可以查看和更新商品目录的移动应用程序。 应用程序要以手持 Windows 为目标吗?是基于 CE 还是 Microsoft?Windows Powered Pocket PC 设备可以作为独立的应用程序操作,也可以作为企业范围的应用程序的一部分。Visual Studio 中含有 Microsoft .NET Compact Framework,我是否要使用它?为 Pocket PC 设备开发应用程序的 .NET 2003。此应用程序中的一个关键要素是:产品的完整目录可否同步,并存储在手持设备上 SQL Server?CE 的一个实例中。
Pocket PC 设备中的 SQL Server CE 数据库包含 Northwind 数据库的一个复制版本,后者来自后端 SQL Server 数据库。由于数据库存在于两个地方,为了维护两个数据库数据的完整性,我将会使用 SQL Server 的合并复制功能。
首先我来解释如何进行合并复制方案的前提条件,包括可对其进行自定义的几种方式。讨论完如何配置环境之后,将介绍应用程序如何运行和利用 SQL Server CE 和 ADO.NET 的断开连接功能。因为在任何应用程序中,异常处理都是很重要的。但是,在开发移动设备时,不仅需要强有力的异常处理,而且需要量是非常巨大的。最后,我将介绍如何捕获手持设备和后端数据库之间可能发生的各种错误,以及如何报告错误。
配置
使用手持设备上的 .NET Compact Framework 和 SQL Server CE 开发应用程序需要一些前提条件,在进行之前,理解这些前提条件是很重要的。.NET Compact Framework 和 SQL Server CE 设备组件现在安装了 Visual Studio .NET 2003。SQL Server CE 服务器组件可以从 Microsoft 网站随着任何 service pack 一起下载,强烈建议您安装此组件。安装好 .NET Compact Framework、SQL Server CE 和最新版本的 SQL Server 和 SQL Server CE service pack 之后,您就可以进行下一步工作了。
虽然安装很简单,但配置合并复制却需要格外注意。在设计应用程序时,以下这点很重要:回顾各个步骤,看看各个移动部件之间是如何交互的。例如,一个简单但容易被忽视的步骤是确保 SQL Server 和 SQL Server Agent 服务同时在后端服务器运行。在样本库存应用程序中,后端 SQL Server 实例中的 Northwind 数据库是中心数据存储,它包含产品、订单和顾客在内的所有信息。应用程序允许手持设备存储来自 Northwind 数据库(位于后端数据库服务器)的库存信息。两个数据库通过合并复制进程进行通信,此进程使用 SQL Server CE Server Agent 来回传递信息。
SQL Server CE Server Agent
SQL Server CE Server Agent 是一个 ISAPI DLL,它是在 Microsoft Internet 信息服务 (IIS) 下的虚拟目录中建立的。实质上,它使用 HTTP 协议传递消息,从而使得两个数据库可以相互通信。要想设置 SQL Server CE Server Agent,最简单的方法是使用 SQL Server CE 虚拟目录创建向导。更高级的方法是手动设置。我在服务器上创建一个文件夹“C:\Projects\SQLCE”,并将 SQL Server CE 安装文件夹(位于 \Program Files\Microsoft SQL Server CE 2.0\Server 下)中的代理 DLL (sscesa20.dll) 复制到此文件夹,然后在那里注册它。然后运用 HTTP 执行权利创建一个虚拟目录(名为 SQLCE),并将其指向 SQL Server CE Server Agent 的新内容文件夹位置。
当计划通过 SQL Server CE Server Agent 实现合并复制时,在 NTFS 文件夹和虚拟目录中都建立合适的权限是很重要的。NTFS 文件夹权限和虚拟目录都应该允许手持应用程序访问此代理。您不应该允许匿名身份验证模式,但在我的示例中,为了简单起见,我允许匿名用户通过 IUSR_machine name 帐户访问虚拟目录。因为在合并复制同步时,会对文件执行写操作,所以我也赋予 IUSR_machine name 帐户对此文件夹的写访问权限。再次强调一下,示例应用程序允许匿名访问,但在实际的应用程序中,应该使用更加安全的身份验证方法。有关连接性和安全性的更多信息,请参阅 Configuring Security for Connectivity。
复制发布
一旦 SQL Server 和 SQL Server CE 实例实现相互通信的机制建立之后,还需要某些东西才能让它们真正能够交流。起初,必须在 SQL Server 后端创建发布来定义合并复制进程的参数。发布定义了可用的数据,以及如果解决任何可能存在的复制冲突。发布访问列表定义允许谁进行订阅。用于订阅的身份验证类型是由 SQL Server 配置定义的。发布可以通过启动创建发布向导来创建;右键单击一个数据库,然后从弹出菜单中选择 New | Publication 即可访问此向导。向导的大多数选项都是很直观的,所以可以专注于一些比较重要的设置。
可以创建的有三种发布类型:快照、事务性的和合并(请参见 图 1)。由于库存应用程序必须允许后端数据库或手持设备的数据库更新数据并相互同步,所以,对于账单使用合并发布最为合适。另外,合并发布是 SQL Server CE 唯一支持的复制形式。
图 1 三种发布类型
向导的下一步指明允许什么类型的订户订阅发布。请确保至少选择“Devices running SQL Server CE”。在此必须指明要发布哪些文章。文章可以是表、视图,甚至是存储过程。在此示例应用程序中,为简单起见,我选择发布所有信息;然而,由于我只打算让手持设备使用与库存相关的信息,所以部分表就已足够。
将发布设置为使用合并复制的一个副作用是:所有选择要发布的文章,如果在 ROWGUIDCOL 属性集中没有唯一约束或主键约束,都会添加一个称为 rowguid 的附加列。当获得发布的第一个快照时,会随 rowguid 列添加一个索引(由类型唯一标识符定义)。在文章中,会自动为新行生成唯一标识符值。唯一标识符值用于将后端数据库的文章的行链接到 SQL Server CE 文章的行上。
发布也可以纵向筛选文章(限制文章中的列)或横向筛选文章(限制文章中的行)。这可以通过设置创建发布向导或设置发布的属性来实现。对于此示例应用程序,我没有创建任何筛选器,而是指定所有表都是已发布的文章。一旦向导完成,它就可以创建发布。
当发布创建后,SQL Server 也在幕后创建其他几个对象来支持复制进程。例如,对所有已发布的文章创建触发器,它可以跟踪服务器上文章所做的更改。更改通过视图发送给 MSmerge_contents 和 MSmerge_tombstone 表,它们同样也是由发布创建的。插入和更新在 MSmerge_contents 表中跟踪,而删除则在 MSmerge_tombstone 表中跟踪。这些视图、触发器和表的创建是为了支持由发布定义的复制进程。SQL Server CE 使用的进程与此类似。
一旦创建好发布后(我将它命名为 NorthwindPub),就可以通过发布访问列表来指定可以访问发布的 SQL Server 帐户了。对于我的示例,我创建了一个名为 SqlTestUser 的 SQL Server 登录,并授予它访问 Northwind 数据库的权限。然后,转到 NorthwindPub 发布的属性中的Publication Access List 选项卡。在此,我将 SqlTestUser 添加到可以访问发布的可用帐户列表中(参见图 2)。
图 2 Northwind 发布属性
最后一个步骤是创建第一个快照。在新订户接受更改之前,它必须包含所有表和发布的已定义文章中包含的所有数据。这意味着当您第一次创建快照,然后复制给订户时,所有数据库表都会创建并填充到订户的数据库中(此例中是 SQL Server CE 数据库)。在后面的同步中,发布者和订户数据库之间只会发送修改内容。
一旦设置好 SQL Server CE Server Agent,就创建了发布,定义了所有身份验证类型并建立了初始快照,这样,复制过程就可以开始了。现在,我需要的就仅仅是一个订户了。请进入示例库存应用程序。
应用程序
此库存应用程序是一个用 C# 编写的智能设备应用程序。由于我有一个 Pocket PC,所以我想让应用程序适用于此 Pocket PC 设备,但使之适用于更多基于 Windows CE 的通用设备选项也很容易。应用程序的基本要求包括使 Northwind 后端数据库的库存数据与手持设备同步。当选择一件商品时,其价格和库存级别都会显示在文本框中,所以用户可以编辑。
使用 .NET Compact Framework 进行开发的一个很大的好处是您不需要针对基于 Windows CE 的设备进行开发。相反,您可以使用 Pocket PC 模拟器来开发和调试。此专栏显示的图片是在模拟器调试模式下的应用程序。
图 3 同步结果
应用程序启动时,它会去查看 Northwind 数据库是否存在于 SQL Server CE 实例中。应用程序第一次运行时,数据还未被复制到 SQL Server CE,所以输入框是空的。当用户按下 Synch 按钮,应用程序会启动合并复制进程,将 NorthwindPub 发布的首个快照传递过来。由于首个快照包含所有的 Northwind 数据,所以它会比后面的同步慢。一旦发布对订户进行了发布,就会弹出一个消息框来显示同步结果(参见图 3)。将显示初始快照中发布者更改的行数,以及发布者与零订户更改的零冲突。首次同步还会在基于 Windows CE 的设备中创建 SQL Server CE 数据库文件来存储 Northwind 数据。
图 4 显示的数据
一旦创建了数据库,应用程序就会在组合框控件中显示类别列表。图 4 显示了在 Beverages 类别中如何选择 Chai 产品以及显示 Chai 产品的库存级别和单价。此处,用户可以浏览和/或编辑其他类别的产品。当用户更改库存级别后,数据就会保存到本地数据集中。一般来说,数据集在同步之后从本地 SQL Server CE 数据库加载。然后数据集会绑定到产品的控件上,并跟踪用户所作的任何数据更改。
图 5 更改库存值
一旦用户按下 Save 按钮,所有更改都会利用 ADO.NET 发送到本地 SQL Server CE 数据库。存储在数据集中的更改也会通过 SqlCeDataAdapter 的 Update 方法发送到数据库。接下来,如果用户按下 Synch 按钮,对本地 SQL Server CE 数据库(订户数据库)进行的更改就发会送到后端数据库(发布者数据库),如果发布者数据库也进行过更改,则更改也会通过合并复制发送到订户数据库。图 5 显示了用户更改一个库存值,但后端数据库没有数据更改的结果。
代码
要使之生效,编码时还需要在项目中包含几个命名空间。智能设备应用程序需要的基本命名空间是 System、System.Drawing、System.Collections、System.ComponentModel 和 System.Windows.Forms。
包含的其他几个命名空间是用于将数据库文件写入手持设备、与 SQL Server CE 数据库通信,以及在数据集中存储产品数据。因此,本项目还需要以下命名空间: using System.Data.SqlServerCe;using System.Data;using System.Data.Common;using System.Text;using System.Xml;
当应用程序第一次启动时,与数据库的连接是使用 SqlCeConnection 对象定义的。该对象的作用与 SqlConnection 对象相似,唯一区别在于它用于与 SQL Server CE 数据库通信。由于它只需操作数据库文件,所以它的数据源和连接字符串比 SqlConnection 对象的连接字符串简单得多。同时建立的还有 SqlCeDataAdapter,并且使用相关的 SELECT 和 UPDATE SQL 语句设置它的 SelectCommand 和 UpdateCommand 属性。由于该应用程序不允许用户对库存数据执行这些操作,所以不需要设置 DeleteCommand 和 InsertCommand 属性。
一旦应用程序启动,它就会尝试从本地数据库加载这些数据。首先,使用 SqlCeCommand 和 SqlCeDataReader 检索类别列表。该类的构造函数会执行并设置应用程序要用到的 ADO.NET 对象。SqlCeCommand 定义检索类别的 SQL 语句。打开 SqlCeConnection 并调用其 ExecuteReader 方法之后,SqlCeCommand 就会返回一个指向类别数据的 SqlCeDataReader。然后,迭代使用 SqlCeDataReader 来将类别数据添加到 cboCategory 组合框中(参见图 6)。
注意:上述方法通过将 Category 类的实例添加到组合框将各项添加到图 6 中的组合框控件。Category 类有 CategoryName 和 CategoryID 两个公共属性。CategoryName 显示在组合框中,而 CategoryID 则用于标识唯一类别。它还有一个 ToString 方法,它重写对象的默认 ToString 方法来返回 CategoryName。这就是 cboCategory 组合框控件知道显示哪个值的原因。
当应用程序加载时,也会检索产品,并将商品的完整列表存储在数据集中。为了检索所有产品,在类的构造函数中已经建立了 SqlCeDataAdapter,所以可以调用 SqlCeDataAdapter 的 Fill 方法来加载数据集,如下所示: this.m_oDA.Fill(this.m_oDs, "Products");
请注意在图 6 中,当类别加载到 cboCategory 组合框后,列表中的第一个类别处于选中状态。这会导致 FilterProducts 方法被调用,它对产品数据集进行筛选,只显示属于选定类别的产品。图 7 显示了 FilterProducts 方法中的主要代码,此方法最先截获选定的类别。其实现方法是将 cboCategory 组合框控件的 SelectedItem 属性转换为 Category 类。然后创建一个 DataView 对象,它根据选定的 CategoryID 值筛选产品数据集。oDV DataView 并不删除数据集中的数据,而是筛选该数据集,所以通过 DataView,只可以访问想要的产品。
接下来,删除所有控件的绑定,并重置为新的 DataView 对象。这个过程允许应用程序一次将所有产品加载到数据集,但只对用户显示经过筛选的部分数据。请记住,用户对数据所做的任何挂起更改仍被跟踪,不管数据在当前 DataView 中是否可见。例如,我们假设用户在 Beverages 类别下更改了 Chai 产品的 reorder 级别,然后切换至 Seafood 类别。虽然 Chai 产品已经不在当前 DataView 中,但仍在数据集中。只有用户按下 Cancel 按钮(它会调用数据集的 RejectChanges 方法)或点击 Save 按钮,否则挂起更改仍将存在,并一直存在。
当用户点击 Save 按钮时,第一步是确保用户的最后一次编辑已被捕获。这是通过截获当前在绑定控件中可见行的行数来保证的。然后使用行数来引用数据集的 Products 表的当前行,以便调用 EndEdit。该方法告诉 DataRow 行的编辑已完成,所以如果有任何更改,都已被捕获: //— Make sure the last edit was captured.int nPosition = BindingContext[this.m_oDs.Tables["Products"]].Position;this.m_oDs.Tables["Products"].Rows[nPosition].EndEdit();
在库存应用程序中,会发生三种不同级别的保存:本地保存到数据集、从数据集到本地 SQL Server CE 数据库,或者通过合并复制在本地 SQL Server CE 数据库和后端 SQL Server 数据库之间。
当用户按下 Synch 按钮时,会触发合并复制进程。图 8 所示的代码实例化 SqlCeReplication 对象(System.Data.SqlServerCe 命名空间的一部分),并设置在后端数据库和本地数据库之间发生合并复制需要的所有属性。Publisher 属性被设置为发布所在服务器的名称。PublisherLogin 和 PublisherPassword 属性被设置为我创建并授予其发布权限的 SQL Server 帐户。InternetUrl、InternetLogin 和 InternetPassword 属性则用于告诉订户向哪里发送请求来合并复制。为了发生合并复制,订户需要通过 SQL Server CE Server Agent 来通信,在本应用程序中,它位于 http://lancelot/sqlce/sscesa20.dll。
SqlCeReplication 对象的 Subscriber 属性设置为订户的名称,它用于标识哪个订户正在请求启动合并复制进程。可能有多个订户,他们运行不同的 Pocket PC 设备。所以,订户名称有助于发布者跟踪订户的变动。Publication 和 PublicationDatabase 属性也必须分别设置为发布和数据库的名称,设备需要利用它们来复制。最后,应该将 SubscriberConnectionString 属性设置为指向 SQL Server CE 数据库实例,以便复制进程可以从 SQL Server CE 数据库读取更改内容,并将任何更改发送到数据库。一旦设置好这些属性,就可以调用 SqlCeReplication 对象的 Synchronize 方法,并将数据重新加载到应用程序: // Perform the synchronization and load the dataoRpl.Synchronize();LoadData(false);
异常处理
在 Visual Studio .NET 中开发智能设备应用程序项目的工作可以分成两大类:配置合并复制和编写应用程序。虽然配置合并复制需要谨慎规划,但过程会比较顺利。开发手持设备应用程序的过程与开发 Windows 窗体应用程序的过程非常类似。然而,调试应用程序中的错误却可能让您一筹莫展。调试工具非常有用,但和其他应用程序一样,只有从异常中捕获信息,调试工具才能用上。与开发其他项目类型一样,在为手持设备开发应用程序时,有个可靠的异常处理策略是很重要的。
在考虑所有的复制配置设置、安全需求、身份验证、安装的前提条件和连接问题时,有很多地方会产生错误。能够确定产生错误的原因是很重要的,SqlCeException 对象通常会包含一些有用信息,需要您重点注意。
图 9 显示了在 SqlCeError 对象中迭代使用的 ShowSqlCeErrors 方法,它代表产生的不同错误。ShowSqlCeErrors 方法在 SqlCeError 对象中循环使用,并利用它的几个属性获取详细的错误信息。然后它将错误信息格式化为字符串,并通过 MessageBox 对象向用户显示异常中的每个错误。这个方法很容易实现,建议使用它,因为它可以使您不必将大量时间浪费在寻找异常产生根源上。
小结
在本示例中,我介绍了如何实现用于基于 Windows CE 的设备的应用程序,它允许用户利用断开连接的数据。可以在手持设备上存储、查看和操作数据,然后通过本地网络,甚至是 Internet 来与后端数据库重新同步。本应用程序的基本要素之一是 ADO.NET。它的断开连接特性和 XML 基使得数据可移植,同时又非常有用。使用这些技术和工具,开发人员就可以开发包含移动设备的企业商业解决方案。
请将您想告诉 John 的问题和建议发送至 mmdata@microsoft.com.
John Papa 是个棒球狂热的爱好者,他与两个小女儿、妻子和忠实的狗 Kadi 通常一起在 Yankees 避暑。他撰写过几本有关 ADO、XML 和 SQL Server 方面的书籍,并屡屡在行业会议上(如 VSLive)发言。您可以通过 mmdata@microsoft.com 与他联系。
Figure 6 Loading the Categories // Get the categories via a DataReaderSqlCeCommand oCmd = new SqlCeCommand("SELECT CategoryID, CategoryName FROM Categories " + " ORDER BY CategoryName", this.m_oCn);this.m_oCn.Open();SqlCeDataReader oDR = oCmd.ExecuteReader();// Clear the category list and fill itcboCategory.Items.Clear();while (oDR.Read()){ cboCategory.Items.Add(new Category((string)oDR["CategoryName"], (int)oDR["CategoryID"]));}oDR.Close();this.m_oCn.Close();// Select the first categorycboCategory.SelectedIndex = 0;Figure 7 Filtering the Products // Get the selected category and cast it back to the Category classCategory oCat = (Category)cboCategory.SelectedItem;// Create a DataView that filters the Products DataTable by the // selected categoryDataView oDV = new DataView(this.m_oDs.Tables["Products"], "CategoryID = " + Convert.ToString(oCat.CategoryID), "ProductName", DataViewRowState.CurrentRows);// Clear all bindingscboProduct.DataBindings.Clear();txtUnitPrice.DataBindings.Clear();txtUnitsInStock.DataBindings.Clear();txtUnitsOnOrder.DataBindings.Clear();txtReorderLevel.DataBindings.Clear();// Bind the DataView to the controlscboProduct.DataSource = oDV;cboProduct.DisplayMember = "ProductName";cboProduct.ValueMember = "ProductID";txtUnitPrice.DataBindings.Add("Text", oDV, "UnitPrice");txtUnitsInStock.DataBindings.Add("Text", oDV, "UnitsInStock");txtUnitsOnOrder.DataBindings.Add("Text", oDV, "UnitsOnOrder");txtReorderLevel.DataBindings.Add("Text", oDV, "ReorderLevel");// Select the first ProductcboProduct.SelectedIndex = 0;Figure 8 Kicking Off Merge Replication //Set up the Replication properties oRpl = new SqlCeReplication();oRpl.Publisher = "lancelot";oRpl.PublisherLogin = "SqlTestUser";oRpl.PublisherPassword = "hmmm";oRpl.InternetUrl = "http://lancelot/sqlce/sscesa20.dll";oRpl.InternetLogin = "iusr_lancelot"; oRpl.InternetPassword = "";oRpl.Subscriber = "CESubscriberTest";oRpl.Publication = "NorthwindPub";oRpl.PublisherDatabase = "Northwind";oRpl.SubscriberConnectionString = "Provider=Microsoft.SQLSERVER.OLEDB.CE.2.0;Data Source=" + this.m_sDataSource;
|