我们先看一段WEB Service的代码。
[WebMethod]
public DataTable GetInfo()
...{
OleDbConnection nwindConn = new OleDbConnection(
"Provider=Microsoft.Jet.OLEDB.4.0;"
"Data Source=D:\\Northwind\\northwind.mdb;");
OleDbCommand selectCMD =
new OleDbCommand("SELECT CustomerID, CompanyName FROM Customers"
, nwindConn);
selectCMD.CommandTimeout = 30;
OleDbDataAdapter custDA = new OleDbDataAdapter();
custDA.SelectCommand = selectCMD;
DataSet custDS = new DataSet();
custDA.Fill(custDS, "Customers");
return custDS.Tables[0];
}
在.net 1.1 中,这是典型的一个错误,在.net 1.1 、1.0中,WEB Service 的返回或者输入参数不能是 DataTable,这是一个众人皆知的知识点。原因就是 DataTable 不象DataSet那样支持序列化。在.net 1.1中,我们解决这个问题的方法就是使用DataSet。但是使用DataSet 的时候,经常会有一种杀鸡用牛刀的感觉。
附:.net 1.1 中使用DataTable作为WEB Service 返回值会报以下异常:
类型 System.ComponentModel.ISite 的成员 System.ComponentModel.MarshalByValueComponent.Site 是接口,因此无法将其序列化。
在.net 2.0 中,以上同样的代码,则没有任何问题了。原因是2.0中 DataTable实现了序列化、反序列。
在VS2005 Beta2 的文档中,我们可以看到2.0 中 DataTable实现了以下接口:
Explicit Interface Implementations
System.ComponentModel.IListSource.get_ContainsListCollection
System.ComponentModel.IListSource.GetList
System.Xml.Serialization.IXmlSerializable.GetSchema
System.Xml.Serialization.IXmlSerializable.ReadXml
System.Xml.Serialization.IXmlSerializable.WriteXml
而在1.1中,DataTable 只实现了一个接口:
Explicit Interface Implementations
System.ComponentModel.IListSource.ContainsListCollection
把DataSet中的一些功能移到 DataTable中,2.0 中还有 Merge 方法,即合并数个数据集。
DataTable的代码合并参看下面代码。
private static void DemonstrateMergeTable()
...{
DataTable table1 = new DataTable("Items");
DataColumn column1 = new DataColumn("id", typeof(System.Int32));
DataColumn column2 = new DataColumn("item", typeof(System.Int32));
table1.Columns.Add(column1);
table1.Columns.Add(column2);
table1.PrimaryKey = new DataColumn[] ...{ column1 };
table1.RowChanged = new System.Data.DataRowChangeEventHandler(Row_Changed);
DataRow row;
for (int i = 0; i <= 3; i )
...{
row = table1.NewRow();
row["id"] = i;
row["item"] = i;
table1.Rows.Add(row);
}
// Accept changes.
table1.AcceptChanges();
DataTable table2 = table1.Clone();
row = table2.NewRow();
row["id"] = 14;
row["item"] = 774;
table2.Rows.Add(row);
row = table2.NewRow();
row["id"] = 12;
row["item"] = 555;
table2.Rows.Add(row);
row = table2.NewRow();
row["id"] = 13;
row["item"] = 665;
table2.Rows.Add(row);
// Merge table2 into the table1.
table1.Merge(table2);
}
综合上述,.net 2.0 中 DataTable 从后台的默默无问的小兵变成独当一面的大将了。
一、简介
大家都知道,目前比较流行的网络浏览器如Mozilla FireFox以及MyIE2等都具有多页面浏览功能,每打开一个新的页面都自动产生一个新的选项卡页面,页面的关闭也十分简便。这种设计思想使得用户在浏览多个网页时桌面十分简洁,也避免了用户等待单页面显示的苦恼。由于这些浏览器一般都支持操作多种文件格式,所以当浏览本地机器上的多个文件时也极为方便。
本文使用Visual C#详细介绍如何实现这种多页面浏览功能。同时,还实现了下面附加功能: 打印, 打印预览,页面属性,选项,查找,查看页面源文件等。
二、关键技术分析
解决问题的关键在于对浏览器控件WebBrowser的NewWindow2事件的编程。当需要显示某种文件而生成一个新窗口时,NewWindow2 事件即被激活。注意,该事件发生在WebBrowser控件的新窗口产生之前。例如,作为对导航到一个新窗口或者一个脚本控制的window.open方法的响应,即发生该事件。为了声明当一个新窗口被打开时,将使用我们自己的浏览器程序,应该把参数ppDisp置为Application 对象。此时,如果你选择“在新窗口中打开”,则新产生一个窗口来显示Web页面。你也可以把RegisterAsBrowser设置为TRUE,这将导致新生成的WebBrowser控件参与到窗口命名的冲突问题上。例如,如果一个窗口的名字在脚本的另外一处用到,那么该控件被派上用场,而不是再产生一个新的窗口,因为控件在打开一个新的窗口之前先检查一下所有已存在的窗口名称以避免命名冲突。 在本文示例中,作为对该事件的响应,我们动态地创建一个tab页面,并通过调用CreateNewWebBrowser()方法产生一个WebBrowser控件作为其子控件――这里每一个子控件都有一个包含该控件相关信息的tag属性。详见下面的源码:
private void axWebBrowser1_NewWindow2(object sender, AxSHDocVw.DWebBrowserEvents2_NewWindow2Event e)
{
AxSHDocVw.AxWebBrowser _axWebBrowser = CreateNewWebBrowser();
e.ppDisp = _axWebBrowser.Application;
_axWebBrowser.RegisterAsBrowser = true;
}
private AxSHDocVw.AxWebBrowser CreateNewWebBrowser()
{
AxSHDocVw.AxWebBrowser _axWebBrowser = new AxSHDocVw.AxWebBrowser();
_axWebBrowser.Tag = new HE_WebBrowserTag();
TabPage _TabPage = new TabPage();
_TabPage.Controls.Add(_axWebBrowser);
_axWebBrowser.Dock = DockStyle.Fill;
_axWebBrowser.BeforeNavigate2 = new AxSHDocVw.DWebBrowserEvents2_BeforeNavigate2EventHandler(this.axWebBrowser1_BeforeNavigate2);
_axWebBrowser.DocumentComplete = new AxSHDocVw.DWebBrowserEvents2_DocumentCompleteEventHandler(this.axWebBrowser1_DocumentComplete);
_axWebBrowser.NavigateComplete2 = new AxSHDocVw.DWebBrowserEvents2_NavigateComplete2EventHandler(this.axWebBrowser1_NavigateComplete2);
_axWebBrowser.NavigateError = new AxSHDocVw.DWebBrowserEvents2_NavigateErrorEventHandler(this.axWebBrowser1_NavigateError);
_axWebBrowser.NewWindow2 = new AxSHDocVw.DWebBrowserEvents2_NewWindow2EventHandler(this.axWebBrowser1_NewWindow2);
_axWebBrowser.ProgressChange = new AxSHDocVw.DWebBrowserEvents2_ProgressChangeEventHandler(this.axWebBrowser1_ProgressChange);
_axWebBrowser.StatusTextChange = new AxSHDocVw.DWebBrowserEvents2_StatusTextChangeEventHandler(this.axWebBrowser1_StatusTextChange);
_axWebBrowser.TitleChange = new AxSHDocVw.DWebBrowserEvents2_TitleChangeEventHandler(this.axWebBrowser1_TitleChange);
_axWebBrowser.CommandStateChange = new AxSHDocVw.DWebBrowserEvents2_CommandStateChangeEventHandler(this.axWebBrowser1_CommandStateChange);
tabControl1.TabPages.Add(_TabPage);
tabControl1.SelectedTab = _TabPage;
return _axWebBrowser;
}
注意,每一个WebBrowser控件都有一个tag,我定义成一个简单的class,它用来包含一些该控件相关的独有信息。请看:
public class HE_WebBrowserTag
{
public int _TabIndex = 0;
public bool _CanBack = false;
public bool _CanForward = false;
}
三、实现“查找”、“查看页面源文件”、“选项”对话框等功能
注意 本例程中使用了一个未公开的GUID,其在将来的系统中可以发生变更。
1、定义 IOleCommandTarget 接口
为定义一个.NET接口以获得关于一个COM接口的参考,请遵从下列步骤:
1) 赋予.NET接口相应的COM接口的GUID值;
2) 包含对接口中所有方法的类型声明;
3) 包含对Mshtml.dll和Shdocvw.dll文件的参考,在Visual C# .NET工程中操作,请遵从:
A. 在项目菜单下单击“添加引用”;
B. 单击“COM” 选项卡;
C. 双击“Microsoft HTML Object Library” 和“Microsoft Internet Controls”。
4) 应该在程序命名空间声明之前,包含下面的接口声明以添加对Microsoft HTML (MSHTML) IOleCommandTarget接口的参照引用:
using System;
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
public struct OLECMDTEXT
{
public uint cmdtextf;
public uint cwActual;
public uint cwBuf;
[MarshalAs(UnmanagedType.ByValTStr,SizeConst=100)]public char rgwz;
}
[StructLayout(LayoutKind.Sequential)]
public struct OLECMD
{
public uint cmdID;
public uint cmdf;
}
// IOleCommandTarget的Interop定义
[ComImport,
Guid("b722bccb-4e68-101b-a2bc-00aa00404770"),
InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IOleCommandTarget
{
//重要: 下面方法的顺序非常重要,因为本示例中我们使用的是早期绑定,详见MSDN中有关.NET/COM互操作的参考。
void QueryStatus(ref Guid pguidCmdGroup, UInt32 cCmds,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=1)] OLECMD[] prgCmds, ref OLECMDTEXT CmdText);
void Exec(ref Guid pguidCmdGroup, uint nCmdId, uint nCmdExecOpt, ref object pvaIn, ref object pvaOut);
}
2、为CGID_IWebBrowser定义一个GUID
必须定义CGI_IWebBrowser的GUID以通知MSHTML如何来处理你的命令ID。在.NET中实现如下:
private Guid cmdGuid = new Guid("ED016940-BD5B-11CF-BA4E-00C04FD70816");
private enum MiscCommandTarget { Find = 1, ViewSource, Options }
3、调用Exec()方法
注意,下列三个过程成功调用Exec()的前提是,已经存在名为webBrowser的浏览器控件的被包容实例。
private mshtml.HTMLDocument GetDocument()
{
try
{
mshtml.HTMLDocument htm = (mshtml.HTMLDocument)axWebBrowser2.Document;
return htm;
}
catch
{
throw (new Exception("不能从WebBrowser控件中获取文件对象"));
}
}
//查看源码的方法
public void ViewSource()
{
IOleCommandTarget cmdt;
Object o = new object();
try
{
cmdt = (IOleCommandTarget)GetDocument();
cmdt.Exec(ref cmdGuid, (uint)MiscCommandTarget.ViewSource,
(uint)SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref o, ref o);
}
catch(Exception e)
{
System.Windows.Forms.MessageBox.Show(e.Message);
}
}
public void Find()
{
IOleCommandTarget cmdt;
Object o = new object();
try
{
cmdt = (IOleCommandTarget)GetDocument();
cmdt.Exec(ref cmdGuid, (uint)MiscCommandTarget.Find,
(uint)SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref o, ref o);
}
catch(Exception e)
{
System.Windows.Forms.MessageBox.Show(e.Message);
}
}
//显示“选项”对话框的方法
public void InternetOptions()
{
IOleCommandTarget cmdt;
Object o = new object();
try
{
cmdt = (IOleCommandTarget)GetDocument();
cmdt.Exec(ref cmdGuid, (uint)MiscCommandTarget.Options,
(uint)SHDocVw.OLECMDEXECOPT.OLECMDEXECOPT_DODEFAULT, ref o, ref o);
}
catch
{
// 注意:因为该过程相应的CMDID是在Internet Explorer处理
// ,所以此处的异常代码块将总被激活,即使该对话框及其操作成功。
//当然,你可以通过浏览器选择设置来禁止这种错误的出现。
//不过,即使出现这种提示,对你的主机也无任何损害。
}
}
四、总结
本文通过C#编例,详细介绍如何实现一种多页面浏览程序的基本原理。欢迎同仁批评指正。 另外,本文所附源程序在Windows 2000/.Net 2003/Internet Explorer 6平台上调试通过。
--1:存储过程返回DataSet 的例子:
C# 代码 :通用类
public static int SingleSTCD(DateTime StartTime,DateTime EndTime,int SMTP,string JL,string STDCD,string STCDSTR, out DataSet ds )
{
int ret=0;
ds=null;
OleDbConnection Constring=new OleDbConnection(Appraise.ConStr);
OleDbCommand cmd=new OleDbCommand("P_Get_STCD_SingleAppraise_XunQi",Constring);
try
{
OleDbDataAdapter ada=new OleDbDataAdapter();
cmd.CommandType=CommandType.StoredProcedure;
cmd.Parameters.Add("@StartTime", OleDbType.DBTimeStamp).Value=StartTime ;
cmd.Parameters.Add("@EndTime", OleDbType.DBTimeStamp).Value=EndTime ;
cmd.Parameters.Add("@SMTP", OleDbType.Integer).Value=SMTP ;
cmd.Parameters.Add("@JL", OleDbType.VarChar, 50).Value=JL ;
//cmd.Parameters.Add("@STDCD", OleDbType.VarChar, 50).Value=STDCD ; //评价标准
cmd.Parameters.Add("@STCDSTR", OleDbType.VarChar, 8000).Value=STCDSTR ;
System.Data.DataSet data = new DataSet();
ada.SelectCommand = cmd;
ada.Fill(data,"table");
ds = data;
ret=ds.Tables[0].Rows.Count;
if ( ret ==-1 )
{
return -1000; //无数据
}
else
{
return ret;
}
}
catch(Exception e)
{
//数据库操作发生错误,返回错误代码
System.Diagnostics.Debug.WriteLine(e.Message);
return -1001;
}
finally
{
Constring.Close();
cmd.Parameters.Clear();
}
VB.net 调用:
Private Sub BindingDataGrid_Query()
Dim ds As System.Data.DataSet
Zehua.Water.Appraise.SingleSTCD("1999-5-1", "1999-9-1", 1, "Ⅲ", "", "50181300,50185050,50181350,50185100,50185150,50185200,50185250,50181200,50185150,51282300", ds)
DataGrid_Query.DataSource = ds
DataGrid_Query.DataBind()
End Sub
--1:存储过程返回数组字符串
REM 执行存储过程,返回一数组
Public Shared Function ExecuteSP_ToArrary(ByVal sConnString As String, ByVal Text_STCD As String, ByVal Text_TIME As String, ByRef TableInfo() As String)
Dim conn As OleDbConnection = New OleDbConnection(sConnString)
ReDim TableInfo(2)
Try
conn.Open()
Dim command As OleDbCommand = New OleDbCommand("P_GET_QueryValue", conn)
command.CommandType = CommandType.StoredProcedure
Dim sqlParams() As OleDbParameter = {New OleDbParameter("@STCD", Text_STCD), New OleDbParameter("@GETM", Text_TIME), New OleDbParameter("@ReturnValue", OleDbType.VarChar, 8000), New OleDbParameter("@ReturnColumnCode", OleDbType.VarChar, 8000), New OleDbParameter("@ReturnColumnName", OleDbType.VarChar, 8000)}
sqlParams(2).Direction = ParameterDirection.Output
sqlParams(3).Direction = ParameterDirection.Output
sqlParams(4).Direction = ParameterDirection.Output
Dim i As Integer = 0
While i < sqlParams.Length
command.Parameters.Add(sqlParams(i))
i = 1
End While
command.ExecuteNonQuery()
If sqlParams(2).Value.ToString().Length > 0 Then
TableInfo(0) = sqlParams(2).Value.ToString()
Else
End If
If sqlParams(3).Value.ToString().Length > 0 Then
TableInfo(1) = sqlParams(3).Value.ToString()
Else
End If
If sqlParams(4).Value.ToString().Length > 0 Then
TableInfo(2) = sqlParams(4).Value.ToString()
Else
End If
Catch ex As Exception
ZeHua.Log.exNoteBugs(ex, "SigleStcdEdit.aspx") 'sb.Append(ex.Message)
Finally
conn.Close()
End Try
End Function
- 多重选择集
- 加入多个实体
- 组
- 扩展记录
- 扩展数据
- 复制和移动
- 镜像拷贝
- 偏移
- 旋转
- 缩放
- 加入符号表记录
- 获取符号表遍历器
- 选择过一个点的所有实体
本章将讨论AutoCAD中的事件。我们将介绍事件处理函数的使用,特别是监视AutoCAD命令的事件处理函数和监视被AutoCAD命令修改的对象的事件处理函数。在解释怎样实现AutoCAD的事件处理之前,我们将首先简要地讨论一下.NET中的事件。
第一部分 VB.NET中的事件
事件只是用来通知一个行为已经发生的信息。在ObjectARX中,我们使用反应器(reactor)来处理AutoCAD的事件。而在AutoCAD .NET API中,ObjectARX反应器被换成了事件。
事件处理函数(或者叫回调函数)是用来监视和反馈程序中出现的事件。事件可以以不同的形式出现。
在介绍AutoCAD .NET API中的事件之前,让我们先来简单地了解一下代理。
第1a部分 代理
代理是一个存储方法索引的类(概念与函数指针类似)。代理对方法是类型安全的(与C中的函数指针类似)。代理有特定的形式和返回类型。代理可以封装符合这种特定形式的任何方法。
代理的一个用途就是作为产生事件的类的分发器。事件是.NET环境中第一级别的对象。虽然VB.NET把事件处理的许多细节给隐藏掉了,但事件总是由代理来实现的。事件代理可以多次调用(就是它们可以存储多于1个的事件处理方法的索引)。它们保存了用于事件的一个注册事件处理的列表。一个典型的代理有以下的形式:
Public Delegate Event (sender as Object, e as EventArgs)
第一个参数sender表示引发事件的对象。第二个参数e是一个EventArgs参数(或者是一个派生的类),这个对象通常包含用于事件处理函数的数据。
第1b部分 AddHandler和RemoveHandler语句
要使用事件处理函数,我们必须把它与事件联系起来。这要通过使用AddHandler语句。AddHandler和RemoveHandler允许你在运行时连接、断开或修改与事件联系的处理函数。
当我们使用AddHandler语句时,我们要确定事件引发者的名字,并要使用AddressOf语句来确定事件处理函数,例如:
AddHandler MyClass1.AnEvent, AddressOf EHandler
前面我们说过要使用RemoveHandler语句从事件处理函数中断开事件(移除联系)。语法如下所示:
RemoveHandler MyClass1.AnEvent, AddressOf EHandler
第2部分 处理.NET中的AutoCAD事件
在ObjectARX中,我们使用反应器来封装AutoCAD事件。在AutoCAD .NET API中,我们可以使用事件来代替ObjectARX反应器。
通常,处理AutoCAD事件的步骤如下:
1. 创建事件处理函数
当一个事件发生时,事件处理函数(或称为回调函数)被调用。任何我们想要处理的回应AutoCAD事件的动作都在事件处理函数中进行。
例如,假定我们只想通知用户一个AutoCAD对象已被加入。我们可以使用AutoCAD数据库事件”ObjectAppended”来完成。我们可以编写回调函数(事件处理函数)如下:
Sub objAppended(ByVal o As Object, ByVal e As ObjectEventArgs)
MessageBox.Show("ObjectAppended!")
‘在这里加入一些代码
End Sub
函数中的第一个参数代表AutoCAD数据库。第二个参数代表ObjectEventArgs类,它可能包含对处理函数有用的数据。
2. 把事件处理函数与事件联系起来
为了开始监视动作,我们必须把事件处理函数与事件联系起来。在这里,当一个对象加入到数据库时,ObjectAppended事件将会发生。但是,事件处理函数不会响应这个事件,除非我们把它与这个事件联系起来,例如:
Dim db As Database
db = HostApplicationServices.WorkingDatabase()
AddHandler db.ObjectAppended, New ObjectEventHandler(AddressOf objAppended)
3. 断开事件处理函数
要终止监视一个动作,我们必须断开事件处理函数与事件的联系。当对象被加入时,我们想要停止通知用户这个事件,我们要断开事件处理函数与事件ObjectAppended的联系。
RemoveHandler db.ObjectAppended, AddressOf objAppended
第3部分 使用事件处理函数来控制AutoCAD的行为
本章的目的是解释AutoCAD事件怎样才能被用于控制AutoCAD图形中的行为。现在,让我们使用前一章(第六章)的内容在AutoCAD图形中创建几个EMPLOYEE块索引。我们不想让用户能改变EMPLOYEE块索引的位置,而对于其它的非EMPLOYEE块索引的位置则没有这个限制。我们将混合使用数据库与文档事件来做到这一点。
首先,我们想要监视将要被执行的AutoCAD命令(使用CommandWillStart事件)。特别地,我们要监视MOVE命令。另外,当一个对象要被修改时,我们应该被通知(使用ObjectOpenedForModify事件),这样我们可以确定它是否为一个EMPLOYEE块索引。如果这时就修改对象可能是无效的,因为我们的修改可能会再次触发事件,从而引起不稳定的行为。所以,我们要等待Move命令的执行结束(使用CommandEnded事件),这时就可以安全地修改对象了。当然,任何对块索引的修改将会触发ObjectOpenedForModify事件。我们还需要设置一些全局变量来表明一个MOVE命令在运行和被修改的对象是一个EMPLOYEE块索引。
注意:因为本章需要比较多的代码来获得想要的结果,所以我们不会解释任何与事件处理无关的代码,而只是将它们粘贴到事件处理函数中。这里的重点是成功创建和注册事件处理函数。
第一步:创建新工程
我们以第六章的工程开始。请新加入一个类AsdkClass2。我们还要加入四个全局变量。前两个是Boolean型的:一个用来表示我们监视的命令是否是活动的,另外一个用来表示ObjectOpenedForModify事件处理函数是否该被忽略。
'全局变量
Dim bEditCommand As Boolean
Dim bDoRepositioning As Boolean
接下来,我们要声明一个全局变量来表示一个ObjectIdCollection,它用来存储我们所选择的要修改的对象的ObjectID。
Dim changedObjects As New ObjectIdCollection()
最后,我们要声明一个全局变量来表示一个Point3dCollection,它用来包含我们所选对象的位置(三维点)。
Dim employeePositions As New Point3dCollection()
第2步:创建第一个文档事件处理函数(回调函数)
现在我们要创建一个事件处理函数。当AutoCAD命令开始执行的时候它会通知我们。我们要检查GlobalCommandName的值是否为MOVE。
If e.GlobalCommandName = "MOVE" Then
'Set the global variables
‘
‘
‘'Delete all stored information
‘
‘
End If
如果MOVE命令开始执行的话,我们要相应地设置Boolean变量bEditCommand的值,这样我们可以知道我们所监视的命令是活动的。同样地,我们应该把另外一个Boolean变量bDoRepositioning设置为false来忽略ObjectOpenedForModify事件处理函数。两个变量设置好以后,在命令活动期间,我们必须要获得所选块索引的信息。
我们还应该把两个集合对象的内容清空。我们只关心当前选择的对象。
第3步: 创建数据库事件处理函数(回调函数)
无论什么时候一个对象被打开并要被修改时,数据库事件处理函数会被调用。当然,如果这时我们监视的命令不是活动的,我们就应该跳过任何被这个回调函数调用的内容。
If bEditCommand = False Then
Return
End If
同样地,如果我们监视的命令已经结束,而ObjectOpenedForModify事件被另一个回调函数再次触发的话,而这时有对象被修改时,我们要阻止所有由这个回调函数执行的动作。
If bDoRepositioning = True Then
Return
End If
这个回调函数剩余部分的代码用来验证我们是否正在处理EMPLOYEE块索引。如果是的话,我们就获取它的ObjectID和位置(三维点)。下面的代码可以被粘贴到这个事件处理函数函数。
Public Sub objOpenedForMod(ByVal o As Object, ByVal e As ObjectEventArgs)
If bEditCommand = False Then
Return
End If
If bDoRepositioning = True Then
Return
End If
Dim objId As ObjectId
objId = e.DBObject.ObjectId
Dim trans As Transaction
Dim bt As BlockTable
Dim db As Database
db = HostApplicationServices.WorkingDatabase
trans = db.TransactionManager.StartTransaction()
Try
'Use it to open the current object!
Dim ent As Entity = trans.GetObject(objId, OpenMode.ForRead, False)
If TypeOf ent Is BlockReference Then 'We use .NET's RTTI to establish type.
Dim br As BlockReference = CType(ent, BlockReference)
'Test whether it is an employee block
'open its extension dictionary
If br.ExtensionDictionary().IsValid Then
Dim brExtDict As DBDictionary = trans.GetObject(br.ExtensionDictionary(), OpenMode.ForRead)
If brExtDict.GetAt("EmployeeData").IsValid Then
'successfully got "EmployeeData" so br is employee block ref
'Store the objectID and the position
changedObjects.Add(objId)
employeePositions.Add(br.Position)
'Get the attribute references,if any
Dim atts As AttributeCollection
atts = br.AttributeCollection
If atts.Count > 0 Then
Dim attId As ObjectId
For Each attId In atts
Dim att As AttributeReference
att = trans.GetObject(attId, OpenMode.ForRead, False)
changedObjects.Add(attId)
employeePositions.Add(att.Position)
Next
End If
End If
End If
End If
trans.Commit()
Finally
trans.Dispose()
End Try
End Sub
第4步 创建第二个文档事件处理函数(回调函数)
当一个命令结束时,第三个事件处理函数被调用。同样地,我们要检查全局变量来验证这个将要结束的命令是我们监视的命令。如果是我们监视的,那么我们要重置这个变量:
If bEditCommand = False Then
Return
End If
bEditCommand = False
这个回调函数执行的动作将会再次触发ObjectOpenedForModify事件。我们必须确定在这个回调函数中跳过了所有与此事件有关的动作。
'设置标志来跳过OpenedForModify处理函数
bDoRepositioning = True
这个回调函数的剩余代码用来把EMPLOYEE块索引和它的关联属性引用的当前(修改过的)位置与它们的初始位置作比较。如果位置改变了,我们在这个回调函数中把它们重置这初始的位置。下面的代码可以被粘贴到这个事件处理函数中。
Public Sub cmdEnded(ByVal o As Object, ByVal e As CommandEventArgs)
'Was our monitored command active?
If bEditCommand = False Then
Return
End If
bEditCommand = False
'Set flag to bypass ObjectOpenedForModify handler
bDoRepositioning = True
Dim db As Database = HostApplicationServices.WorkingDatabase
Dim trans As Transaction
Dim bt As BlockTable
Dim oldpos As Point3d
Dim newpos As Point3d
Dim i As Integer
Dim j As Integer = 1
For i = 0 To changedObjects.Count - 1
trans = db.TransactionManager.StartTransaction()
Try
bt = trans.GetObject(db.BlockTableId, OpenMode.ForRead)
Dim ent As Entity = CType(trans.GetObject(changedObjects.Item(i), OpenMode.ForWrite), Entity)
If TypeOf ent Is BlockReference Then 'We use .NET's RTTI to establish type.
Dim br As BlockReference = CType(ent, BlockReference)
newpos = br.Position
oldpos = employeePositions.Item(i)
'Reset blockref position
If Not oldpos.Equals(newpos) Then
trans.GetObject(br.ObjectId, OpenMode.ForWrite)
br.Position = oldpos
End If
ElseIf TypeOf ent Is AttributeReference Then
Dim att As AttributeReference = CType(ent, AttributeReference)
newpos = att.Position
oldpos = employeePositions.Item(i)
'Reset attref position
If Not oldpos.Equals(newpos) Then
trans.GetObject(att.ObjectId, OpenMode.ForWrite)
att.Position = oldpos
End If
End If
bt.Dispose()
trans.Commit()
Finally
trans.Dispose()
End Try
Next
End Sub
第5步 创建命令来注册/断开事件处理函数
创建一个ADDEVENTS命令,使用 =语句来把上面的3个事件处理函数连接到各自的事件。在这个命令中,我们还应该设置全局Boolean变量:
bEditCommand = False
bDoRepositioning = False
创建另外一个命令REMOVEEVENTS,使用RemoveHandler语句把事件处理函数与事件断开。
第6步: 测试工程
要测试这个工程,请使用CREATE命令创建一个或多个EMPLOYEE块索引。如果你要作比较的话,你也可以插入一些非EMPLOYEE的块索引。
在命令行中键入ADDEVENTS命令来执行它。
在命令行中输入MOVE命令,然后选择你想要的块索引。注意,当MOVE命令结束时,EMPLOYEE块索引(包括属性)还留在原处。
执行REMOVEEVENTS命令,然后在试一下MOVE命令。注意,EMPLOYEE块索引现在可以被移动了。
附加的问题:添加一个附加的回调函数,当用户改变EMPLOYEE块索引的”Name”属性时,这个回调函数被触发。
本章将讨论AutoCAD中的事件。我们将介绍事件处理函数的使用,特别是监视AutoCAD命令的事件处理函数和监视被AutoCAD命令修改的对象的事件处理函数。在解释怎样在C#中实现AutoCAD的事件处理之前,我们将首先简要地讨论一下.NET中的事件。
第一部分 C#中的事件
事件只是用来通知一个行为已经发生的信息。在ObjectARX中,我们使用反应器(reactor)来处理AutoCAD的事件。而在AutoCAD .NET API中,ObjectARX反应器被换成了事件。
事件处理函数(或者叫回调函数)是用来监视和反馈程序中出现的事件。事件可以以不同的形式出现。
在介绍AutoCAD .NET API中的事件之前,让我们先来简单地了解一下代理。
第1a部分 代理
代理是一个存储方法索引的类(概念与函数指针类似)。代理对方法是类型安全的(与C中的函数指针类似)。代理有特定的形式和返回类型。代理可以封装符合这种特定形式的任何方法。
代理的一个用途就是作为产生事件的类的分发器。事件是.NET环境中第一级别的对象。虽然C#把事件处理的许多细节给隐藏掉了,但事件总是由代理来实现的。事件代理可以多次调用(就是它们可以存储多于1个的事件处理方法的索引)。它们保存了用于事件的一个注册事件处理的列表。一个典型的代理有以下的形式:
public delegate Event (Object sender, EventArgs e)
第一个参数sender表示引发事件的对象。第二个参数e是一个EventArgs参数(或者是一个派生的类),这个对象通常包含用于事件处理函数的数据。
第1b部分 =和-=语句
要使用事件处理函数,我们必须把它与事件联系起来。这要通过使用 =语句。 =和-=允许你在运行时连接、断开或修改与事件联系的处理函数。
当我们使用 =语句时,我们要确定事件引发者的名字,并要使用new语句来确定事件处理函数,例如:
MyClass1.AnEvent = new HandlerDelegate(EHandler)
前面我们说过要使用-=语句从事件处理函数中断开事件(移除联系)。语法如下所示:
MyClass1.AnEvent -= new HandlerDelegate(EHandler)
第2部分 处理.NET中的AutoCAD事件
在ObjectARX中,我们使用反应器来封装AutoCAD事件。在AutoCAD .NET API中,我们可以使用事件来代替ObjectARX反应器。
通常,处理AutoCAD事件的步骤如下:
1. 创建事件处理函数
当一个事件发生时,事件处理函数(或称为回调函数)被调用。任何我们想要处理的回应AutoCAD事件的动作都在事件处理函数中进行。
例如,假定我们只想通知用户一个AutoCAD对象已被加入。我们可以使用AutoCAD数据库事件”ObjectAppended”来完成。我们可以编写回调函数(事件处理函数)如下:
public void objAppended(object o, ObjectEventArgs e)
{
// 在这里加入处理代码
}
函数中的第一个参数代表AutoCAD数据库。第二个参数代表ObjectEventArgs类,它可能包含对处理函数有用的数据。
2. 把事件处理函数与事件联系起来
为了开始监视动作,我们必须把事件处理函数与事件联系起来。在这里,当一个对象加入到数据库时,ObjectAppended事件将会发生。但是,事件处理函数不会响应这个事件,除非我们把它与这个事件联系起来,例如:
Database db;
db = HostApplicationServices.WorkingDatabase;
db. ObjectAppended = new ObjectEventHandler(objAppended);
3. 断开事件处理函数
要终止监视一个动作,我们必须断开事件处理函数与事件的联系。当对象被加入时,我们想要停止通知用户这个事件,我们要断开事件处理函数与事件ObjectAppended的联系。
db. ObjectAppended -= new ObjectEventHandler(objAppended);
第3部分 使用事件处理函数来控制AutoCAD的行为
本章的目的是解释AutoCAD事件怎样才能被用于控制AutoCAD图形中的行为。现在,让我们使用前一章(第六章)的内容在AutoCAD图形中创建几个EMPLOYEE块索引。我们不想让用户能改变EMPLOYEE块索引的位置,而对于其它的非EMPLOYEE块索引的位置则没有这个限制。我们将混合使用数据库与文档事件来做到这一点。
首先,我们想要监视将要被执行的AutoCAD命令(使用CommandWillStart事件)。特别地,我们要监视MOVE命令。另外,当一个对象要被修改时,我们应该被通知(使用ObjectOpenedForModify事件),这样我们可以确定它是否为一个EMPLOYEE块索引。如果这时就修改对象可能是无效的,因为我们的修改可能会再次触发事件,从而引起不稳定的行为。所以,我们要等待Move命令的执行结束(使用CommandEnded事件),这时就可以安全地修改对象了。当然,任何对块索引的修改将会触发ObjectOpenedForModify事件。我们还需要设置一些全局变量来表明一个MOVE命令在运行和被修改的对象是一个EMPLOYEE块索引。
注意:因为本章需要比较多的代码来获得想要的结果,所以我们不会解释任何与事件处理无关的代码,而只是将它们粘贴到事件处理函数中。这里的重点是成功创建和注册事件处理函数。
第一步:创建新工程
我们以第六章的工程开始。请新加入一个类AsdkClass2。我们还要加入四个全局变量。前两个是Boolean型的:一个用来表示我们监视的命令是否是活动的,另外一个用来表示ObjectOpenedForModify事件处理函数是否该被忽略。
//全局变量
bool bEditCommand;
bool bDoRepositioning;
接下来,我们要声明一个全局变量来表示一个ObjectIdCollection,它用来存储我们所选择的要修改的对象的ObjectID。
ObjectIdCollection changedObjects = new ObjectIdCollection();
最后,我们要声明一个全局变量来表示一个Point3dCollection,它用来包含我们所选对象的位置(三维点)。
Point3dCollection employeePositions = new Point3dCollection();
第2步:创建第一个文档事件处理函数(回调函数)
现在我们要创建一个事件处理函数。当AutoCAD命令开始执行的时候它会通知我们。我们要检查GlobalCommandName的值是否为MOVE。
if ( e.GlobalCommandName == "MOVE" )
{
}
如果MOVE命令开始执行的话,我们要相应地设置Boolean变量bEditCommand的值,这样我们可以知道我们所监视的命令是活动的。同样地,我们应该把另外一个Boolean变量bDoRepositioning设置为false来忽略ObjectOpenedForModify事件处理函数。两个变量设置好以后,在命令活动期间,我们必须要获得所选块索引的信息。
我们还应该把两个集合对象的内容清空。我们只关心当前选择的对象。
第3步: 创建数据库事件处理函数(回调函数)
无论什么时候一个对象被打开并要被修改时,数据库事件处理函数会被调用。当然,如果这时我们监视的命令不是活动的,我们就应该跳过任何被这个回调函数调用的内容。
if ( bEditCommand == false )
{
return;
}
同样地,如果我们监视的命令已经结束,而ObjectOpenedForModify事件被另一个回调函数再次触发的话,而这时有对象被修改时,我们要阻止所有由这个回调函数执行的动作。
if ( bDoRepositioning == true )
{
return;
}
这个回调函数剩余部分的代码用来验证我们是否正在处理EMPLOYEE块索引。如果是的话,我们就获取它的ObjectID和位置(三维点)。下面的代码可以被粘贴到这个事件处理函数函数。
public void objOpenedForMod(object o, ObjectEventArgs e)
{
if ( bEditCommand == false )
{
return;
}
if ( bDoRepositioning == true )
{
return;
}
ObjectId objId;
objId = e.DBObject.ObjectId;
Transaction trans;
Database db;
db = HostApplicationServices.WorkingDatabase;
trans = db.TransactionManager.StartTransaction();
using(Entity ent = (Entity)trans.GetObject(objId, OpenMode.ForRead, false))
{
if ( ent.GetType().FullName.Equals( "Autodesk.AutoCAD.DatabaseServices.BlockReference" ) )
{ //We use .NET//s RTTI to establish type.
BlockReference br = (BlockReference)ent;
//Test whether it is an employee block
//open its extension dictionary
if ( br.ExtensionDictionary.IsValid )
{
using(DBDictionary brExtDict = (DBDictionary)trans.GetObject(br.ExtensionDictionary, OpenMode.ForRead))
{
if ( brExtDict.GetAt("EmployeeData").IsValid )
{
//successfully got "EmployeeData" so br is employee block ref
//Store the objectID and the position
changedObjects.Add(objId);
employeePositions.Add(br.Position);
//Get the attribute references,if any
AttributeCollection atts;
atts = br.AttributeCollection;
if ( atts.Count > 0 )
{
foreach(ObjectId attId in atts )
{
AttributeReference att;
using(att = (AttributeReference)trans.GetObject(attId, OpenMode.ForRead, false))
{
changedObjects.Add(attId);
employeePositions.Add(att.Position);
}
}
}
}
}
}
}
}
trans.Commit();
}
第4步 创建第二个文档事件处理函数(回调函数)
当一个命令结束时,第三个事件处理函数被调用。同样地,我们要检查全局变量来验证这个将要结束的命令是我们监视的命令。如果是我们监视的,那么我们要重置这个变量:
if ( bEditCommand == false )
{
return;
}
bEditCommand = false;
这个回调函数执行的动作将会再次触发ObjectOpenedForModify事件。我们必须确定在这个回调函数中跳过了所有与此事件有关的动作。
//设置标志来跳过OpenedForModify处理函数
bDoRepositioning = true;
这个回调函数的剩余代码用来把EMPLOYEE块索引和它的关联属性引用的当前(修改过的)位置与它们的初始位置作比较。如果位置改变了,我们在这个回调函数中把它们重置这初始的位置。下面的代码可以被粘贴到这个事件处理函数中。
public void cmdEnded(object o , CommandEventArgs e)
{
//Was our monitored command active?
if ( bEditCommand == false )
{
return;
}
bEditCommand = false;
//Set flag to bypass OpenedForModify handler
bDoRepositioning = true;
Database db = HostApplicationServices.WorkingDatabase;
Transaction trans ;
BlockTable bt;
Point3d oldpos;
Point3d newpos;
int i ;
for ( i = 0; i< changedObjects.Count; i )
{
trans = db.TransactionManager.StartTransaction();
using( bt = (BlockTable)trans.GetObject(db.BlockTableId, OpenMode.ForRead) )
{
using(Entity ent = (Entity)trans.GetObject(changedObjects[i], OpenMode.ForWrite))
{
if ( ent.GetType().FullName.Equals("Autodesk.AutoCAD.DatabaseServices.BlockReference") )
{ //We use .NET//s RTTI to establish type.
BlockReference br = (BlockReference)ent;
newpos = br.Position;
oldpos = employeePositions[i];
//Reset blockref position
if ( !oldpos.Equals(newpos) )
{
using( trans.GetObject(br.ObjectId, OpenMode.ForWrite) )
{
br.Position = oldpos;
}
}
}
else if ( ent.GetType().FullName.Equals("Autodesk.AutoCAD.DatabaseServices.AttributeReference") )
{
AttributeReference att = (AttributeReference)ent;
newpos = att.Position;
oldpos = employeePositions[i];
//Reset attref position
if ( !oldpos.Equals(newpos) )
{
using( trans.GetObject(att.ObjectId, OpenMode.ForWrite))
{
att.Position = oldpos;
}
}
}
}
}
trans.Commit();
}
}
第5步 创建命令来注册/断开事件处理函数
创建一个ADDEVENTS命令,使用 =语句来把上面的3个事件处理函数连接到各自的事件。在这个命令中,我们还应该设置全局Boolean变量:
bEditCommand = false;
bDoRepositioning = false;
创建另外一个命令REMOVEEVENTS,使用-=语句把事件处理函数与事件断开。
第6步: 测试工程
要测试这个工程,请使用CREATE命令创建一个或多个EMPLOYEE块索引。如果你要作比较的话,你也可以插入一些非EMPLOYEE的块索引。
在命令行中键入ADDEVENTS命令来执行它。
在命令行中输入MOVE命令,然后选择你想要的块索引。注意,当MOVE命令结束时,EMPLOYEE块索引(包括属性)还留在原处。
执行REMOVEEVENTS命令,然后在试一下MOVE命令。注意,EMPLOYEE块索引现在可以被移动了。
附加的问题:添加一个附加的回调函数,当用户改变EMPLOYEE块索引的”Name”属性时,这个回调函数被触发。
摘要:有些情况下,非类型化的 DataSet 可能并非数据操作的最佳解决方案。本指南的目的就是探讨DataSet 的一种替代解决方案,即:自定义实体与集合。(本文包含一些指向英文站点的链接。)
引言
ADODB.RecordSet 和常常被遗忘的 MoveNext 的时代已经过去,取而代之的是 Microsoft ADO.NET 强大而又灵活的功能。我们的新武器就是 System.Data 名称空间,它的特点是具有速度极快的 DataReader 和功能丰富的 DataSet,而且打包在一个面向对象的强大模型中。能够使用这样的工具一点都不奇怪。任何 3 层体系结构都依靠可靠的数据访问层 (DAL) 将数据层与业务层完美地连接起来。高质量的 DAL 有助于改善代码的重新使用,它是获得高性能的关键,而且是完全透明的。
随着工具的改进,我们的开发模式也发生了变化。告别 MoveNext 并不只是让我们摆脱了繁琐的语法,它还让我们认识了断开连接的数据,这种数据对我们开发应用程序的方式产生了深刻的影响。
因为我们已经熟悉了 DataReader(其行为与 RecordSet 非常类似),所以没花多长时间就进一步开发出 DataAdapter、DataSet、DataTable 和 DataView。正是在开发这些新对象的过程中不断得到磨炼的技能改变了我们的开发方式。断开连接的数据使我们可以利用新的缓存技术,从而大大提高了应用程序的性能。这些类的功能使我们能够编写出更智能、更强大的函数,同时还能减少(有时候甚至是大大减少)常见活动所需的代码数量。
有些情况下非常适合使用 DataSet,例如在设计原型、开发小型系统和支持实用程序时。但是,在企业系统中使用 DataSet 可能并不是最佳的解决方案,因为对企业系统来说,易于维护要比投入市场的时间更重要。本指南的目的就是探讨一种适合处理此类工作的 DataSet 的替代解决方案,即:自定义实体与集合。尽管还存在其他替代解决方案,但它们都无法提供相同的功能或无法获得更多的支持。我们的首要任务是了解 DataSet 的缺点,以便理解我们要解决的问题。
记住,每种解决方案都有优缺点,所以 DataSet 的缺点可能比自定义实体的缺点(我们也将进行讨论)更容易让您接受。您和您的团队必须自己决定哪个解决方案更适合您的项目。记住要考虑解决方案的总成本,包括要求改变的实质所在以及生产后所需的时间比实际开发代码的时间更长的可能性。最后请注意,我所说的 DataSet 并不是类型化的 DataSet,但它确实可以弥补非类型化的 DataSet 的一些缺点。
DataSet 存在的问题
缺少抽象
寻找替代解决方案的第一个也是最明显的原因就是 DataSet 无法从数据库结构中提取代码。DataAdapter 可以很好地使您的代码独立于基础数据库供应商(Microsoft、Oracle、IBM 等),但不能抽象出数据库的核心组件:表、列和关系。这些核心数据库组件也是 DataSet 的核心组件。DataSet 和数据库不仅共享通用组件,不幸的是,它们还共享架构。假定有下面这样一个 Select 语句:
SELECT UserId, FirstName, LastName
FROM Users
我们知道这些值可以从 DataSet 中的 UserId、FirstName 和 LastName 这些 DataColumn 中获得。
为什么会这么复杂?让我们看一个基本的日常示例。首先我们有一个简单的 DAL 函数:
'Visual Basic .NET
Public Function GetAllUsers() As DataSet
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As SqlCommand = New SqlCommand("GetUsers", connection)
command.CommandType = CommandType.StoredProcedure
Dim da As SqlDataAdapter = New SqlDataAdapter(command)
Try
Dim ds As DataSet = New DataSet
da.Fill(ds)
Return ds
Finally
connection.Dispose()
command.Dispose()
da.Dispose()
End Try
End Function
//C#
public DataSet GetAllUsers() {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUsers", connection);
command.CommandType = CommandType.StoredProcedure;
SqlDataAdapter da = new SqlDataAdapter(command);
try {
DataSet ds = new DataSet();
da.Fill(ds);
return ds;
}finally {
connection.Dispose();
command.Dispose();
da.Dispose();
}
}
然后我们有一个页面,它使用重复器显示所有用户:
<HTML>
<body>
<form id="Form1" method="post" runat="server">
<asp:Repeater ID="users" Runat="server">
<ItemTemplate>
<%# DataBinder.Eval(Container.DataItem, "FirstName") %>
<br />
</ItemTemplate>
</asp:Repeater>
</form>
</body>
</HTML>
<script runat="server">
public sub page_load
users.DataSource = GetAllUsers()
users.DataBind()
end sub
</script>
正如我们所看到的那样,我们的 ASPX 页面利用 DAL 函数 GetAllUsers 作为重复器的 DataSource。如果由于某种原因(为了性能而降级、为清楚起见而进行了标准化、要求发生了变化)导致数据库架构发生变化,变化就会一直影响 ASPX,即影响使用“FirstName”列名的 Databinder.Eval 行。这将立刻在您脑海中产生一个危险信号:数据库架构的变化会一直影响到 ASPX 代码吗?听起来不太像 N 层,对吗?
如果我们所要做的只是对列进行简单的重命名,那么更改本例中的代码并不复杂。但是,如果在许多地方都使用了 GetAllUsers,更糟糕的是,如果将其作为为无数用户提供服务的 Web 服务,那又会怎么样呢?怎样才能轻松或安全地传播更改?对于这个基本示例而言,存储过程本身作为抽象层可能已经足够;但是依赖存储过程获得除最基本的保护以外的功能则可能会在以后造成更大的问题。可以将此视为一种硬编码;实质上,使用 DataSet 时,您可能需要在数据库架构(不管使用列名称还是序号位置)和应用层/业务层之间建立一个严格的连接。但愿以前的经验(或逻辑)已经让您了解到硬编码对维护工作以及将来的开发产生的影响。
DataSet 无法提供适当抽象的另一个原因是它要求开发人员必须了解基础架构。我们所说的不是基础知识,而是关于列名称、类型和关系的所有知识。去掉这个要求不仅使您的代码不像我们看到的那样容易中断,还使代码更易于编写和维护。简单地说:
Convert.ToInt32(ds.Tables[0].Rows[i]["userId"]);
不仅难于阅读,而且需要非常熟悉列名称及其类型。理想情况下,您的业务层不需要知道有关基础数据库、数据库架构或 SQL 的任何内容。如果您像上述代码字符串中那样使用 DataSet(使用 CodeBehind 并不会有任何改善),您的业务层可能会很薄。
弱类型
DataSet 属于弱类型,因此容易出错,还可能会影响您的开发工作。这意味着无论何时从 DataSet 中检索值,值都以 System.Object 的形式返回,您需要对这种值进行转换。您面临转换可能会失败的风险。不幸的是,失败不是在编译时发生,而是在运行时发生。另外,在处理弱类型的对象时,Microsoft Visual Studio.NET (VS.NET) 等工具对您的开发人员并没有太大的帮助。前面我们说过需要深入了解构架的知识,就是指这个意思。我们再来看一个非常常见的示例:
'Visual Basic.NET
Dim userId As Integer =
? Convert.ToInt32(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)("UserId"))
Dim userId As Integer = CInt(ds.Tables(0).Rows(0)(0))
//C#
int userId = Convert.ToInt32(ds.Tables[0].Rows[0]("UserId"));
这段代码显示了从 DataSet 中检索值的可能方法——可能您的代码中到处都需要检索值(如果不进行转换,而您使用的又是 Visual Basic .NET,您可能会使用 Option Strict Off 这样的代码,而这会给您带来更大的麻烦。)
不幸的是,这些代码中的每一行都可能会产生大量的运行时错误:
1.
转换可能由于以下原因而失败:
• 值可能为空。
• 开发人员可能对基础数据类型判断有误(还是这个问题,即开发人员需要非常熟悉数据库架构)。
• 如果您使用序号值,谁知道位置 X 处实际上是一个什么样的列。
2.
ds.Tables(0) 可能返回一个空引用(如果 DAL 方法或存储过程中有任何部分失败)。
3.
“UserId”可能由于以下原因而是一个无效的列名称:
• 可能已经更改了名称。
• 可能不是由存储过程返回的。
• 可能包含错别字。
我们可以修改代码并以更安全的方式编写,即为 null/nothing 添加检查,为转换添加 try/catch,但这些对开发人员都没有帮助。
更糟糕的是,正如我们前面所说,这不是抽象的。这意味着,每次要从 DataSet 中检索 userId 时,您都将面临上面提到的风险,或者需要对相同的保护性步骤进行重新编程(当然,实用程序功能可能会有助于降低风险)。弱类型对象将错误从设计时或编译时(这时总能够自动检测并轻松修复错误)转移到运行时(这时的错误可能会出现在生产过程中,而且更难查明)。
非面向对象
您不能仅仅因为 DataSet 是对象,而 C# 和 Visual Basic .NET 是面向对象 (OO) 的语言就能以面向对象的方式使用 DataSet。OO 编程的“hello world”是一个典型的 Person 类,该类又是 Employee 的子类。但 DataSet 并没有使此类继承或其他大多数 OO 技术成为可能(或者至少使它们变得自然/直观)。Scott Hanselman 是类实体的坚决支持者,他做出了最好的解释:
“DataSet 是一个对象,对吗?但它并不是域对象,它不是一个‘苹果’或‘桔子’,而是一个‘DataSet’类型的对象。DataSet 是一只碗(它知道支持数据存储)。DataSet 是一个知道如何保存行和列的对象,它非常了解数据库。但是,我不希望返回碗,我希望返回域对象,例如‘苹果’。”1
DataSet 使数据之间保持一种关系,使它们更强大并且能够在关系数据库中方便地使用。不幸的是,这意味着您将失去 OO 的所有优点。
因为 DataSet 不能作为域对象,所以无法向它们添加功能。通常情况下,对象具有字段、属性和方法,它们的行为针对的是类的实例。例如,您可能会将 Promote 或 CalcuateOvertimePay 函数与 User 对象相关联,该对象可以通过 someUser.Promote() 或 someUser.CalculateOverTimePay() 安全地调用。因为无法向 DataSet 添加方法,所以您需要使用实用程序功能来处理弱类型对象,并且在整个代码中包含硬编码值的更多实例。您一般会以过程代码结束,在过程代码中,您要么不断地从 DataSet 中获取数据,要么以繁琐的方式将它们存储在本地变量中并向其他位置传递。两种方法都有缺点,而且都没有任何优点。
与 DataSet 相反的情况
如果您认为数据访问层应返回 DataSet,您可能会漏掉一些重要的优点。其中一个原因是您可能正在使用一个较薄或不存在的业务层,除了其他问题外,它还限制了您进行抽象的能力。另外,因为您使用的是一般的预编译解决方案,所以很难利用 OO 技术。最后,Visual Studio.NET 等工具使开发人员无法轻松地利用弱类型对象(例如 DataSet),因此降低了效率并且增加了出错的可能性。
所有这些因素都以不同的方式对代码的可维护性产生了直接的影响。缺乏抽象使功能改善和错误修复变得更复杂、更危险。您无法充分利用 OO 提供的代码重新使用或可读性方面的改进。当然还有一点,无论您的开发人员处理的是业务逻辑还是表示逻辑,他们都必须非常了解您的基础数据结构。
自定义实体类
与 DataSet 有关的大多数问题都可以利用 OO 编程的丰富功能在定义明确的业务层中解决。实际上,我们希望获得按照关系组织的数据(数据库),并将数据作为对象(代码)使用。这个概念就是,不是获得保存汽车信息的 DataTable,而是获得汽车对象(称为自定义实体或域对象)。
在了解自定义实体之前,让我们首先看一看我们将要面临的挑战。最明显的挑战就是所需代码的数量。我们不是简单地获取数据并自动填充 DataSet,而是获取数据并手动将数据映射到自定义实体(必须先创建好)。由于这是一项重复性的任务,我们可以使用代码生成工具或 O/R 映射器(后文有详细的介绍)来减轻工作量。更大的问题是将数据从关系世界映射到对象世界的具体过程。对于简单的系统,映射通常是直接的,但是随着复杂性的增加,这两个世界之间的差异就会产生问题。例如,继承在对象世界中是获得代码重新使用以及可维护性的重要技术。不幸的是,继承对关系数据库来说却是一个陌生的概念。另外一个例子就是处理关系的方式不同:对象世界依靠维护单个对象的引用,而关系世界则是利用外键。
因为代码的数量以及关系数据和对象之间的差异不断增加,看起来这个方法并不太适合更复杂的系统,但事实正好相反。通过将各种问题隔离到一个层中,即映射过程(同样可以自动化),复杂的系统也可以从此方法获益。另外,此方法已经很常用,这意味着可以通过几种已有的设计模式彻底解决增加的复杂性。前面讨论的 DataSet 的缺点在复杂系统中将成倍扩大,最后您会得出这样一个系统,它欠缺灵活应变能力的缺点恰好超出其构建的难度。
什么是自定义实体?
自定义实体是代表业务域的对象,因此,它们是业务层的基础。如果您有一个用户身份验证组件(本指南通篇都使用该示例进行讲解),您就可能具有 User 和 Role 对象。电子商务系统可能具有 Supplier 和 Merchandise 对象,而房地产公司则可能具有 House、Room 和 Address 对象。在您的代码中,自定义实体只是一些类(实体和“类”之间具有非常密切的关系,就像在 OO 编程中使用的那样)。一个典型的 User 类可能如下所示:
'Visual Basic .NET
Public Class User
#Region "Fields and Properties"
Private _userId As Integer
Private _userName As String
Private _password As String
Public Property UserId() As Integer
Get
Return _userId
End Get
Set(ByVal Value As Integer)
_userId = Value
End Set
End Property
Public Property UserName() As String
Get
Return _userName
End Get
Set(ByVal Value As String)
_userName = Value
End Set
End Property
Public Property Password() As String
Get
Return _password
End Get
Set(ByVal Value As String)
_password = Value
End Set
End Property
#End Region
#Region "Constructors"
Public Sub New()
End Sub
Public Sub New(id As Integer, name As String, password As String)
Me.UserId = id
Me.UserName = name
Me.Password = password
End Sub
#End Region
End Class
//C#
public class User {
#region "Fields and Properties"
private int userId;
private string userName;
private string password;
public int UserId {
get { return userId; }
set { userId = value; }
}
public string UserName {
get { return userName; }
set { userName = value; }
}
public string Password {
get { return password; }
set { password = value; }
}
#endregion
#region "Constructors"
public User() {}
public User(int id, string name, string password) {
this.UserId = id;
this.UserName = name;
this.Password = password;
}
#endregion
}
为什么能够从它们获益?
使用自定义实体获得的主要好处来自这样一个简单的事实,即它们是完全受您控制的对象。具体而言,它们允许您:
• 利用继承和封装等 OO 技术。
• 添加自定义行为。
例如,我们的 User 类可以通过为其添加 UpdatePassword 函数而受益(我们可能会使用外部/实用程序函数对数据集执行此类操作,但会影响可读性/维护性)。另外,它们属于强类型,这表示我们可以获得 IntelliSense 支持:
图 1:User 类的 IntelliSense
最后,因为自定义实体为强类型,所以不太需要进行容易出错的强制转换:
Dim userId As Integer = user.UserId
'与
Dim userId As Integer =
? Convert.ToInt32(ds.Tables("users").Rows(0)("UserId"))
对象关系映射
正如前文所讨论的那样,此方法的主要挑战之一就是处理关系数据和对象之间的差异。因为我们的数据始终存储在关系数据库中,所以我们只能在这两个世界之间架起一座桥梁。对于上文的 User 示例,我们可能希望在数据库中建立一个如下所示的用户表:
图 2:User 的数据视图
从这个关系架构映射到自定义实体是一个非常简单的事情:
'Visual Basic .NET
Public Function GetUser(ByVal userId As Integer) As User
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetUserById", connection)
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader(CommandBehavior.SingleRow)
If dr.Read Then
Dim user As New User
user.UserId = Convert.ToInt32(dr("UserId"))
user.UserName = Convert.ToString(dr("UserName"))
user.Password = Convert.ToString(dr("Password"))
Return user
End If
Return Nothing
Finally
If Not dr is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public User GetUser(int userId) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUserById", connection);
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader(CommandBehavior.SingleRow);
if (dr.Read()){
User user = new User();
user.UserId = Convert.ToInt32(dr["UserId"]);
user.UserName = Convert.ToString(dr["UserName"]);
user.Password = Convert.ToString(dr["Password"]);
return user;
}
return null;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
我们仍然按照通常的方式设置连接和命令对象,但接着创建了 User 类的一个新实例并从 DataReader 中填充该实例。您仍然可以在此函数中使用 DataSet 并将其映射到您的自定义实体,但 DataSet 相对于 DataReader 的主要好处是前者提供了数据的断开连接的视图。在本例中,User 实例提供了断开连接的视图,使我们可以利用 DataReader 的速度。
等一下!您并没有解决任何问题!
细心的读者可能注意到我前面提到 DataSet 的问题之一是它们并非强类型,这导致效率降低并增加了出现运行时错误的可能性。它们还需要开发人员深入了解基础数据结构。看一看上文的代码,您可能会注意到这些问题依然存在。但请注意,我们已经将这些问题封装到一个非常孤立的代码区域内;这表示您的类实体的使用者(Web 界面、Web 服务使用者、Windows 表单)仍然完全没有意识到这些问题。相反,使用 DataSet 可以将这些问题分散到整个代码中。
改进
上文的代码对显示映射的基本概念很有用,但可以在两个关键的方面进行改进。首先,我们需要提取并将代码填充到其自己的函数中,因为代码有可能会被重新使用:
'Visual Basic .NET
Public Function PopulateUser(ByVal dr As IDataRecord) As User
Dim user As New User
user.UserId = Convert.ToInt32(dr("UserId"))
'检查 NULL 的示例
If Not dr("UserName") Is DBNull.Value Then
user.UserName = Convert.ToString(dr("UserName"))
End If
user.Password = Convert.ToString(dr("Password"))
Return user
End Function
//C#
public User PopulateUser(IDataRecord dr) {
User user = new User();
user.UserId = Convert.ToInt32(dr["UserId"]);
//检查 NULL 的示例
if (dr["UserName"] != DBNull.Value){
user.UserName = Convert.ToString(dr["UserName"]);
}
user.Password = Convert.ToString(dr["Password"]);
return user;
}
第二个需要注意的事项是,我们不对映射函数使用 SqlDataReader,而是使用 IDataRecord。这是所有 DataReader 实现的接口。使用 IDataRecord 使我们的映射过程独立于供应商。也就是说,我们可以使用上一个函数从 Access 数据库中映射 User,即使它使用 OleDbDataReader 也可以。如果您将这个特定的方法与 Provider Model Design Pattern(链接 1、链接 2)结合使用,您的代码就可以轻松地用于不同的数据库提供程序。
最后,以上代码说明了封装的强大功能。处理 DataSet 中的 NULL 并非最简单的事,因为每次提取值时都需要检查它是否为 NULL。使用上述填充方法,我们在一个地方就轻松地解决了此问题,使我们的客户无需处理它。
映射到何处?
关于此类数据访问和映射函数的归属问题存在一些争论,即究竟是作为独立类的一部分,还是作为适当自定义实体的一部分。将所有用户相关的任务(获取数据、更新和映射)都作为 User 自定义实体的一部分当然很不错。这在数据库架构与自定义实体很相似时会很有用(比如在本例中)。随着系统复杂性的增加,这两个世界的差异开始显现出来,将数据层和业务层明确分离对简化维护有很大的帮助(我喜欢将其称为数据访问层)。将访问和映射代码放在其自己的层 (DAL) 上有一个副作用,即它为确保数据层与业务层的明确分离提供了一个严格的原则:
“永远不要从 System.Data 返回类或从 DAL 返回子命名空间”
自定义集合
到目前为止,我们只了解了如何处理单个实体,但您经常需要处理多个对象。一个简单的解决方案是将多个值存储在一个一般的集合(例如 Arraylist)中。这并非最理想的解决方案,因为它又产生了与 DataSet 有关的一些问题,即:
• 它们不是强类型,并且
• 无法添加自定义行为。
最能满足我们需求的解决方案是创建我们自己的自定义集合。幸亏 Microsoft .NET Framework 提供了一个专门为了此目的而继承的类:CollectionBase。CollectionBase 的工作原理是,将所有类型的对象都存储在专有 Arraylist 中,但是通过只接受特定类型(例如 User 对象)的方法来提供对这些专有集合的访问。也就是说,将弱类型代码封装在强类型的 API 中。
虽然自定义集合可能看起来有很多代码,但大多数都可以由代码生成功能或通过剪切和粘贴方便地完成,并且通常只需要一次搜索和替换即可。让我们看一看构成 User 类的自定义集合的不同部分:
'Visual Basic .NET
Public Class UserCollection
Inherits CollectionBase
Default Public Property Item(ByVal index As Integer) As User
Get
Return CType(List(index), User)
End Get
Set
List(index) = value
End Set
End Property
Public Function Add(ByVal value As User) As Integer
Return (List.Add(value))
End Function
Public Function IndexOf(ByVal value As User) As Integer
Return (List.IndexOf(value))
End Function
Public Sub Insert(ByVal index As Integer, ByVal value As User)
List.Insert(index, value)
End Sub
Public Sub Remove(ByVal value As User)
List.Remove(value)
End Sub
Public Function Contains(ByVal value As User) As Boolean
Return (List.Contains(value))
End Function
End Class
//C#
public class UserCollection :CollectionBase {
public User this[int index] {
get {return (User)List[index];}
set {List[index] = value;}
}
public int Add(User value) {
return (List.Add(value));
}
public int IndexOf(User value) {
return (List.IndexOf(value));
}
public void Insert(int index, User value) {
List.Insert(index, value);
}
public void Remove(User value) {
List.Remove(value);
}
public bool Contains(User value) {
return (List.Contains(value));
}
}
通过实现 CollectionBase 可以完成更多任务,但上面的代码代表了自定义集合所需的核心功能。观察一下 Add 函数,可以看出我们只是简单地将对 List.Add(它是一个 Arraylist)的调用封装到仅允许 User 对象的函数中。
映射自定义集合
将我们的关系数据映射到自定义集合的过程与我们对自定义实体执行的过程非常相似。我们不再创建一个实体并将其返回,而是将该实体添加到集合中并循环到下一个:
'Visual Basic .NET
Public Function GetAllUsers() As UserCollection
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetAllUsers", connection)
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader(CommandBehavior.SingleResult)
Dim users As New UserCollection
While dr.Read()
users.Add(PopulateUser(dr))
End While
Return users
Finally
If Not dr Is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public UserCollection GetAllUsers() {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command =new SqlCommand("GetAllUsers", connection);
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader(CommandBehavior.SingleResult);
UserCollection users = new UserCollection();
while (dr.Read()){
users.Add(PopulateUser(dr));
}
return users;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
我们从数据库中获得数据、创建自定义集合,然后通过在结果中循环来创建每个 User 对象并将其添加到集合中。同样要注意 PopulateUser 映射函数是如何重新使用的。
添加自定义行为
在讨论自定义实体时,我们只是泛泛地提到可以将自定义行为添加到类中。您向实体中添加的功能类型很大程度上取决于您要实现的业务逻辑的类型,但您可能希望在自定义集合中实现某些常见的功能。一个示例就是返回一个基于某个键的实体,例如基于 userId 的用户:
'Visual Basic .NET
Public Function FindUserById(ByVal userId As Integer) As User
For Each user As User In List
If user.UserId = userId Then
Return user
End If
Next
Return Nothing
End Function
//C#
public User FindUserById(int userId) {
foreach (User user in List) {
if (user.UserId == userId){
return user;
}
}
return null;
}
另一个示例可能是返回基于特定标准(例如部分用户名)的用户子集:
'Visual Basic .NET
Public Function FindMatchingUsers(ByVal search As String) As UserCollection
If search Is Nothing Then
Throw New ArgumentNullException("search cannot be null")
End If
Dim matchingUsers As New UserCollection
For Each user As User In List
Dim userName As String = user.UserName
If Not userName Is Nothing And userName.StartsWith(search) Then
matchingUsers.Add(user)
End If
Next
Return matchingUsers
End Function
//C#
public UserCollection FindMatchingUsers(string search) {
if (search == null){
throw new ArgumentNullException("search cannot be null");
}
UserCollection matchingUsers = new UserCollection();
foreach (User user in List) {
string userName = user.UserName;
if (userName != null && userName.StartsWith(search)){
matchingUsers.Add(user);
}
}
return matchingUsers;
}
可以通过 DataTable.Select 以相同的方式使用 DataSets。需要说明的重要一点是,尽管创建自己的功能使您可以完全控制您的代码,但 Select 方法为完成同样的操作提供了一个非常方便且不需要编写代码的方法。但另一方面,Select 需要开发人员了解基础数据库,而且它不是强类型。
绑定自定义集合
我们看到的第一个示例是将 DataSet 绑定到 ASP.NET 控件。考虑到它很普通,您会高兴地发现自定义集合绑定同样很简单(这是因为 CollectionBase 实现了用于绑定的 Ilist)。自定义集合可以作为任何控件的 DataSource,而 DataBinder.Eval 只能像您使用 DataSet 那样使用:
'Visual Basic .NET
Dim users as UserCollection = DAL.GetallUsers()
repeater.DataSource = users
repeater.DataBind()
//C#
UserCollection users = DAL.GetAllUsers();
repeater.DataSource = users;
repeater.DataBind();
<!-- HTML -->
<asp:Repeater onItemDataBound="r_IDB" ID="repeater" Runat="server">
<ItemTemplate>
<asp:Label ID="userName" Runat="server">
<%# DataBinder.Eval(Container.DataItem, "UserName") %><br />
</asp:Label>
</ItemTemplate>
</asp:Repeater>
您可以不使用列名称作为 DataBinder.Eval 的第二个参数,而指定您希望显示的属性名称,在本例中为 UserName。
对于在许多数据绑定控件提供的 OnItemDataBound 或 OnItemCreated 中执行处理的人来说,您可能会将 e.Item.DataItem 强制转换成 DataRowView。当绑定到自定义集合时,e.Item.DataItem 则被强制转换成自定义实体,在我们的示例中为 User 类:
'Visual Basic .NET
Protected Sub r_ItemDataBound (s As Object, e As RepeaterItemEventArgs)
Dim type As ListItemType = e.Item.ItemType
If type = ListItemType.AlternatingItem OrElse
? type = ListItemType.Item Then
Dim u As Label = CType(e.Item.FindControl("userName"), Label)
Dim currentUser As User = CType(e.Item.DataItem, User)
If Not PasswordUtility.PasswordIsSecure(currentUser.Password) Then
ul.ForeColor = Drawing.Color.Red
End If
End If
End Sub
//C#
protected void r_ItemDataBound(object sender, RepeaterItemEventArgs e) {
ListItemType type = e.Item.ItemType;
if (type == ListItemType.AlternatingItem ||
? type == ListItemType.Item){
Label ul = (Label)e.Item.FindControl("userName");
User currentUser = (User)e.Item.DataItem;
if (!PasswordUtility.PasswordIsSecure(currentUser.Password)){
ul.ForeColor = Color.Red;
}
}
}
管理关系
即使在最简单的系统中,实体之间也存在关系。对于关系数据库,可以通过外键维护关系;而使用对象时,关系只是对另一个对象的引用。例如,根据我们前面的示例,User 对象完全可以具有一个 Role:
'Visual Basic .NET
Public Class User
Private _role As Role
Public Property Role() As Role
Get
Return _role
End Get
Set(ByVal Value As Role)
_role = Value
End Set
End Property
End Class
//C#
public class User {
private Role role;
public Role Role {
get {return role;}
set {role = value;}
}
}
或者一个 Role 集合:
'Visual Basic .NET
Public Class User
Private _roles As RoleCollection
Public ReadOnly Property Roles() As RoleCollection
Get
If _roles Is Nothing Then
_roles = New RoleCollection
End If
Return _roles
End Get
End Property
End Class
//C#
public class User {
private RoleCollection roles;
public RoleCollection Roles {
get {
if (roles == null){
roles = new RoleCollection();
}
return roles;
}
}
}
在这两个示例中,我们有一个虚构的 Role 类或 RoleCollection 类,它们就是类似于 User 和 UserCollection 类的其他自定义实体或集合类。
映射关系
真正的问题在于如何映射关系。让我们看一个简单的示例,我们希望根据 userId 及其角色来检索一个用户。首先,我们看一看关系模型:
图 3:User 与 Role 之间的关系
这里,我们看到了一个 User 表和一个 Role 表,我们可以将这两个表都以直观的方式映射到自定义实体。我们还有一个 UserRoleJoin 表,它代表了 User 与 Role 之间的多对多关系。
然后,我们使用存储过程来获取两个单独的结果:第一个代表 User,第二个代表该用户的 Role:
CREATE PROCEDURE GetUserById(
@UserId INT
)AS
SELECT UserId, UserName, [Password]
FROM Users
WHERE UserId = @UserID
SELECT R.RoleId, R.[Name], R.Code
FROM Roles R INNER JOIN
UserRoleJoin URJ ON R.RoleId = URJ.RoleId
WHERE URJ.UserId = @UserId
最后,我们从关系模型映射到对象模型:
'Visual Basic .NET
Public Function GetUserById(ByVal userId As Integer) As User
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("GetUserById", connection)
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId
Dim dr As SqlDataReader = Nothing
Try
connection.Open()
dr = command.ExecuteReader()
Dim user As User = Nothing
If dr.Read() Then
user = PopulateUser(dr)
dr.NextResult()
While dr.Read()
user.Roles.Add(PopulateRole(dr))
End While
End If
Return user
Finally
If Not dr Is Nothing AndAlso Not dr.IsClosed Then
dr.Close()
End If
connection.Dispose()
command.Dispose()
End Try
End Function
//C#
public User GetUserById(int userId) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("GetUserById", connection);
command.Parameters.Add("@UserId", SqlDbType.Int).Value = userId;
SqlDataReader dr = null;
try{
connection.Open();
dr = command.ExecuteReader();
User user = null;
if (dr.Read()){
user = PopulateUser(dr);
dr.NextResult();
while(dr.Read()){
user.Roles.Add(PopulateRole(dr));
}
}
return user;
}finally{
if (dr != null && !dr.IsClosed){
dr.Close();
}
connection.Dispose();
command.Dispose();
}
}
User 实例即被创建和填充;我们转移到下一个结果/选择并进行循环,填充 Role 并将它们添加到 User 类的 RolesCollection 属性中。
高级内容
本指南的目的是介绍自定义实体与集合的概念及使用。使用自定义实体是业界广泛采用的做法,因此,也就产生了同样多的模式以处理各种情况。设计模式具有优势的原因有很多。首先,在处理具体的情况时,您可能不是第一次碰到某个给定的问题。设计模式使您可以重新使用给定问题的已经过尝试和测试的解决方案(虽然设计模式并不意味着全盘照抄,但它们几乎总是能够为解决方案提供一个可靠的基础)。相应地,这使您对系统随着复杂性增加而进行缩放的能力充满了信心,不仅因为它是一个广泛使用的方法,还因为它具有详尽的记录。设计模式还为您提供了一个通用的词汇表,使知识的传播和传授更容易实现。
不能说设计模式只适用于自定义实体,实际上许多设计模式都并非如此。但是,如果您找机会试一下,您可能会惊喜地发现许多记载详尽的模式确实适用于自定义实体和映射过程。
最后这一部分专门介绍大型或较复杂的系统可能会碰到的一些高级情况。因为大多数主题都可能值得您单独学习,所以我会尽量为您提供一些入门资料。
Martin Fowler 的 Patterns of Enterprise Application Architecture 就是一个很好的入门材料,它不仅可以作为常见设计模式的优秀参考(具有详细的解释和大量的示例代码),而且它的前 100 页确实可以让您透彻地了解整个概念。另外,Fowler 还提供了一个联机模式目录,它对于已经熟悉概念但需要一个便利参考的人士很有用。
并发
前面的示例介绍的都是从数据库中提取数据并根据这些数据创建对象。总体而言,更新、删除和插入数据等操作是很直观的。我们的业务层负责创建对象、将对象传递给数据访问层,然后让数据访问层处理对象世界与关系世界之间的映射。例如:
'Visual Basic .NET
Public sub UpdateUser(ByVal user As User)
Dim connection As New SqlConnection(CONNECTION_STRING)
Dim command As New SqlCommand("UpdateUser", connection)
' 可以借助可重新使用的函数对此进行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int)
command.Parameters(0).Value = user.UserId
command.Parameters.Add("@Password", SqlDbType.VarChar, 64)
command.Parameters(1).Value = user.Password
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128)
command.Parameters(2).Value = user.UserName
Try
connection.Open()
command.ExecuteNonQuery()
Finally
connection.Dispose()
command.Dispose()
End Try
End Sub
//C#
public void UpdateUser(User user) {
SqlConnection connection = new SqlConnection(CONNECTION_STRING);
SqlCommand command = new SqlCommand("UpdateUser", connection);
// 可以借助可重新使用的函数对此进行反向映射
command.Parameters.Add("@UserId", SqlDbType.Int);
command.Parameters[0].Value = user.UserId;
command.Parameters.Add("@Password", SqlDbType.VarChar, 64);
command.Parameters[1].Value = user.Password;
command.Parameters.Add("@UserName", SqlDbType.VarChar, 128);
command.Parameters[2].Value = user.UserName;
try{
connection.Open();
command.ExecuteNonQuery();
}finally{
connection.Dispose();
command.Dispose();
}
}
但在处理并发时就不那么直观了,也就是说,当两个用户试图同时更新相同的数据时会出现什么情况呢?默认的行为(如果您没有执行任何操作)是最后提交数据的人将覆盖以前所有的工作。这可能不是理想的情况,因为一个用户的工作将在未获得任何提示的情况下被覆盖。要完全避免所有冲突,一种方法就是使用消极的并发技术;但此方法需要具有某种锁定机制,这可能很难通过可缩放的方式实现。替代方法就是使用积极的并发技术。让第一个提交的用户控制并通知后面的用户是通常采取的更温和、更用户友好的方法。这可以通过某种行版本控制(例如时间戳)来实现。
参考资料:
• Introduction to Data Concurrency in ADO.NET
• CSLA.NET's concurrency techniques
• Unit of Work design pattern
• Optimistic offline lock design pattern
• Pessimistic offline lock design pattern
性能
与合理的灵活性和功能问题相对的是,我们经常担心细小的性能差异。尽管性能的确很重要,但提供适用于一切情况而不是最简单情况的通用原则通常很难。例如,将自定义集合与 DataSet 相比,哪个更快?使用自定义集合,您可以大量使用 DataReader,这是从数据库中提取数据的较快方式。但答案实际上取决于您使用它们的方式以及处理的数据类型,所以一般性的说明没有任何用。更重要的一点是要认识到,不管您能节省多少处理时间,与维护性方面的差异相比都可能微不足道。
当然,并不是说您不可能找到一个既具有高性能又可维护的解决方案。虽然我强调说答案实际上取决于您的使用方式,但的确有一些模式可以帮助您最大程度地提高性能。但是,首先要知道的是自定义实体与集合缓存以及 DataSet,并且能够利用相同的机制(类似于 HttpCache)。DataSet 的优势之一是它能够编写 Select 语句,以便只获取所需的信息。使用自定义实体时,您常常感到不得不填充整个实体以及子实体。例如,如果要通过 DataSet 显示一个 Organization 列表,您可以只提取 OganizationId、Name 和 Address 并将其绑定到重复器。使用自定义实体时,我总觉得还需要获取所有其他的 Organization 信息,如果该组织通过了 ISO 认证,则可能是一个位标记,即所有员工、其他联系信息等的集合。可能其他人没有碰到这个大难题,但幸运的是,如果我们愿意,我们可以对自定义实体进行很好的控制。最常用的方法是使用一种延迟加载模式,它只在首次需要时获取信息(可以很好地封装在属性中)。这种对各个属性的控制提供了通过其他方式无法轻易获得的巨大灵活性(请想象一下在 DataColumn 级别执行类似操作的情况)。
参考资料:
• Lazy Load 设计模式
• CSLA.NET lazy load
排序与筛选
虽然 DataView 对排序和筛选的内置支持需要您了解有关 SQL 和基础数据结构的知识,但它提供的方便确实是自定义集合所不具备的。我们仍然可以排序和筛选,但首先需要编写功能。因为技术不一定是最先进的,所以代码的完整描述不属于本节要讨论的范围。大多数技术都很相似,例如使用筛选器类筛选集合以及使用比较器类进行排序,我认为不存在固定的模式。但是,的确存在一些参考资料:
• Generic sort function
• Sorting & Filtering Custom Collections 教程
代码生成
解决概念上的障碍后,自定义实体与集合的主要缺点就是灵活性、抽象和维护性差所导致的代码数量的增加。实际上,您可能会认为我所说的维护成本和错误的降低这一切都抵不上代码的增加。虽然这一观点是成立的(同样,因为任何解决方案都不是完美无缺的),但可以通过设计模式和框架(例如 CSLA.NET)大大缓解此问题。代码生成工具与模式和框架完全不同,这些工具可以大大降低您实际需要编写的代码数量。本指南最初打算专门辟出一节详细介绍代码生成工具



