使用 .NET 2.0 开发下一代智能客户端,并与现有的基于 .NET 1.1 的 SOA XML Web 服务协同工作






4.96/5 (126投票s)
2005 年 7 月 29 日
40分钟阅读

1244408

3878
全面指南,介绍如何开发 .NET 2.0 智能客户端,与现有的基于服务面向架构 (SOA) 的 XML Web 服务协同工作,并充分利用企业库。
- 下载源文件 - 1.68 MB (更新于 2005 年 8 月 17 日)
引言
.NET 2.0 在 Windows Forms 和部署技术方面引入了许多新功能,将智能客户端开发提升到了新的水平。以前,我们在开发智能客户端时不得不处理大量问题,包括丰富的控件(所有者绘制控件)、多线程、自动更新以及其他各种问题。所有这些问题在 2.0 框架中都得到了解决。然而,大多数基于 Web 的系统,包括 Web 服务和网站,都已在 .NET 1.1 中开发。因此,在本文中,我们将研究一个实际场景,其中基于 .NET 2.0 的智能客户端将使用一个使用 .NET 1.1 开发的、经过精心设计的服务面向架构 (SOA) 的 Web 服务集合。示例应用程序将向您展示从早期设计问题到部署复杂性的全过程,以及如何让智能客户端巧妙地与一系列使用著名 SOA 精心设计的 Web 服务进行通信。服务器端将通过使用其所有应用程序块,充分利用 2005 年 6 月的企业库。最终产品是尖端技术和最新架构设计趋势最佳实践的理想示例。
如果您想要一个纯 .NET 1.1 智能客户端,请参阅我的另一篇文章:RSS Feed Aggregator and Blogging Smart Client。
修订
- 2005 年 8 月 9 日 - 在 Microsoft 风格的自动化模型处进一步解释了自动化模型。
- 2005 年 8 月 9 日 - 如何在“添加引用”或“更新引用”后清理 Web 代理类。点击此处。
- 2005 年 8 月 7 日 - 更新了代码和 客户端安装配置。您需要创建一个测试证书才能编译客户端。
功能概述
登录
![]() |
这可不是每天都能看到的东西。它看起来像一个即将发布的 Longhorn(后来重命名为 Windows Vista)的登录屏幕。新的登录屏幕与此不同,但我真的很喜欢这个设计,并决定在我的应用程序中使用它。 当您单击“登录”按钮时,将生成一个后台线程,该线程连接到 Web 服务并传递凭据。由于登录是在后台进行的,因此 UI 保持活动状态,您可以取消登录并退出。我们将看到如何使用新的 ![]() 图:登录进行中 |
异步加载所需数据
![]() |
主窗体加载时,需要加载课程列表和学生列表。由于这需要时间,我们不能让用户卡在 UI 上,因此它通过两个不同的异步 Web 服务调用在后台加载。您可以将小型控件开发得如此出色,以至于每个控件本身都可以利用方便的 在应用程序加载时,UI 始终保持响应。由于这是一项耗时任务,用户可能希望在应用程序准备就绪时探索菜单或处理其他事务。 |
标签式界面:以文档为中心的视图
![]() |
该应用程序遵循我们将在本文中讨论的 Model-View-Controller 架构的增强版本。我实现的架构几乎与 Microsoft Office、Visual Studio 和其他提供自动化支持的桌面应用程序中的架构类似。您将看到如何利用这一出色的设计理念来创建真正解耦、可扩展且可脚本化的应用程序,从而显著节省您的开发和维护时间。 每个选项卡代表一个 模块可以异步加载其数据。例如,此处异步加载了学生的课程列表。尽管所有学生的基本个人资料信息在应用程序启动时加载,但详细信息是按需加载的,从而节省了初始带宽消耗和整个应用程序的总内存分配。 |
所有者绘制的 TreeView
![]() |
我们将探讨 .NET 2.0 的新 WinForms 功能,这些功能使得大多数控件都可以进行所有者绘制。现在您可以创建所有者绘制的 左侧的课程 |
离线功能
![]() |
离线功能是智能客户端的最佳功能。智能客户端可以下载您需要稍后使用的数据,然后断开与信息源的连接。然后,您可以在移动中或不在办公室时处理数据。稍后,您可以连接到信息存储并发送/接收在离线期间所做的更改。 这里的一个例子是,您可以打开需要处理的学生数据。然后您可以断开与 Web 服务的连接或进入离线状态。然后,您可以修改这些学生,方法是编辑个人资料,或添加/删除课程,修改账户等。稍后,当您连接或上线时,单击“发送/接收”按钮,更改将同步回服务器。 您和服务器都会获得最新、最准确的信息。 您可以通过提供本地数据存储来进一步增强离线体验,以便用户可以在离线后关闭应用程序,稍后可以启动应用程序来处理本地数据。这可以通过将课程和学生集合序列化到某个本地 XML 文件来轻松完成。 |
上线
![]() |
这里您看到 Outlook 2003 风格的发送接收模块。此模块收集待定保存并将它们发送到服务器。它还会为已修改的学生获取最新的学生信息。 如果从服务器收到任何错误,您可以在第二个选项卡中看到它们。此选项卡使用所有者绘制的 对于此类离线同步,您需要在服务器端处理并发。稍后我们将讨论服务器架构以及我们如何实现并发。 |
作为智能客户端
要求
为了使应用程序有资格成为智能客户端,根据 MSDN 上智能客户端的定义,应用程序需要满足以下要求:
本地资源和用户体验
所有智能客户端应用程序共享的是利用本地资源(如用于存储、处理或数据捕获的硬件,例如闪存卡、CPU 和扫描仪)的能力。智能客户端解决方案通过充分利用 Microsoft® Windows® 平台提供的所有功能,提供高质量的最终用户体验。众所周知的智能客户端应用程序的例子有 Word、Excel、MS Money,甚至像《半条命 2》这样的 PC 游戏。与 Amazon.Com 或 eBay.com 等“基于浏览器”的应用程序不同,智能客户端应用程序安装在您的 PC、笔记本电脑、Tablet PC 或智能设备上。
已连接
智能客户端应用程序能够轻松地连接到企业系统或 Internet 并与之交换数据。Web 服务允许智能客户端解决方案利用 XML、HTTP 和 SOAP 等行业标准协议与任何类型的远程系统交换信息。
可离线
无论是否连接到 Internet,智能客户端应用程序都可以正常工作。Microsoft® Money 和 Microsoft® Outlook 是两个很好的例子。智能客户端可以利用本地缓存和处理,在网络连接中断或间歇性网络连接期间启用操作。离线功能不仅对移动场景有用,桌面解决方案也可以利用离线架构在后台线程中更新后端系统,从而保持用户界面的响应性并改善整体最终用户体验。这种架构还可以带来成本和性能优势,因为用户界面不必从服务器传输到智能客户端。由于智能客户端可以在后台仅交换所需数据与其他系统,因此与其他系统交换的数据量会减少(即使在硬连线客户端系统上,这种带宽减少也能带来巨大好处)。这反过来又提高了用户界面 (UI) 的响应速度,因为 UI 不是由远程系统渲染的。
智能部署和更新
过去,传统的客户端应用程序难以部署和更新。安装一个应用程序却破坏了另一个应用程序的情况并不少见。“DLL Hell”使安装和维护客户端应用程序变得困难且令人沮丧。来自 Patterns and Practices 团队的 .NETUpdater 应用程序块为那些希望创建跨多个桌面部署的自更新 .NET Framework 应用程序的开发人员提供了规范性指导。Visual Studio 2005 和 .NET Framework 2.0 的发布将通过一个名为 ClickOnce 的新部署和更新技术,开启一个简化智能客户端部署和更新的新时代。
(以上文本摘录并缩短自 MSDN 网站。)
这如何成为一个智能客户端
- 本地资源和用户体验:应用程序通过 .NET 2.0 的 ClickOnce 部署功能从网站下载并在本地运行。它充分利用本地资源、多线程能力、图形卡的强大功能,为您提供 .NET 2.0 所能提供的最佳用户体验。
- 已连接:该应用程序通过调用多个充当客户端后端的 Web 服务来工作。
- 可离线:您可以加载需要处理的学生数据并进入离线状态。您可以修改学生个人资料,添加/删除课程,修改账户等,然后在上线时将它们同步回来。
- 智能部署和更新:该应用程序使用 Updater Application Block 2.0 提供自动更新功能。每当我发布新版本或将某些 bug 修复部署到中央服务器时,该应用程序的所有用户都会在后台自动获得更新。这使得每个用户都不必每次我发布新内容时都去网站下载新版本。它还使我能够非常快速地将 bug 修复即时交付给所有人。
- 多线程:我将客户端真正智能化为智能客户端的另一个首选要求是使其完全多线程。智能客户端应始终保持响应。它们在下载或上传数据时绝不能卡住。用户将继续工作,而不会知道后台正在进行一些重要的事情。例如,在加载一个学生课程列表时,您可以移动到左侧的课程列表,或者处理已加载的另一个学生数据。
- 防崩溃:当智能客户端在用户面前崩溃并显示令人讨厌的“继续”或“退出”对话框时,它就变成了哑客户端。为了使您的应用程序真正智能,您需要捕获任何未处理的错误并安全地发布错误。在此应用程序中,我将向您展示如何使用 .NET 2.0 的未处理异常陷阱功能来实现这一点。
客户端
Microsoft 风格的自动化模型
此应用程序最大的特点是它提供了与 Microsoft Office 应用程序中看到的类似的自动化模型。因此,您可以编写脚本来自动化应用程序,也可以开发自定义插件。这种架构的理念和实现非常宏大,并在本文中进行了详细介绍。
基本思想是创建一个可观察的对象模型,所有 UI 模块或视图都订阅该模型,以便根据对象模型的变化提供服务。例如,一旦您在 Documents
集合中添加了一个新的 Document
对象,Tab 模块就会捕获 Add
事件并创建一个新的选项卡。视图也将 UI 上发生的变化反映到对象模型中。例如,当用户切换选项卡时,视图会将相应 Document
对象的 Selected
属性设置为 true。

这是客户端应用程序的对象模型。_Application
是起点。这不是 WinForm 的 Application
类。这是我自己的 Application
类,带有一个前缀“_”。它包含 StudentModel
、CourseModel
和 Document
的集合。这些都是可观察类,它们提供各种事件的通知。它们都继承自一个名为 ItemBase
的类,该类为可观察对象模型提供了基础。ItemBase
暴露了许多事件,您可以订阅它们以在对象发生任何变化时收到通知。
public abstract class ItemBase : IDisposable, IXml
{
...
...
public virtual event ItemSelectHandler OnSelect;
public virtual event ItemQueryClose OnQueryClose;
/// <summary>
/// Event to notify that the item is changed
/// </summary>
public virtual event ItemChangeHandler OnChange;
/// <summary>
/// Event to notify observers to detach from the item
/// </summary>
public virtual event ItemDetachHandler OnDetach;
/// <summary>
/// Show the item in UI
/// </summary>
public virtual event ItemShowHandler OnShow;
}
当您将对象扩展自 ItemBase
时,您就获得了观察对象变化的特性。您的扩展对象可以调用 ItemBase
的 NotifyChange
方法,以便在对象发生更改时通知他人。例如,让我们看看 StudentModel
,它会监听其属性的任何变化。每次发生变化时,它都会调用 NotifyChange
方法通知其他人它已发生变化。

因此,这为您提供了以下功能:
- 每当您更改
Student
对象中的属性时,例如将FirstName
属性设置为“Misho”(我的名字),UI 上的“First Name”文本框就会更新。这发生是因为包含文本框的StudentProfile
UserControl
已经订阅了接收StudentModel
事件,并且专门用于将对象模型中的更改反映到 UI。 - 同样,每当用户在名字文本框中键入任何内容时,
UserControl
就会立即更新StudentModel
的FirstName
属性。结果,OnChange
事件被触发,并且另一个模块(例如 Shrek2)会收到通知,并更新应用程序标题栏,显示学生的当前姓名。
因此,您现在可以拥有能够广播事件的对象。任何模块都可以随时订阅/取消订阅学生对象,并在根本不知道其他模块存在的情况下提供某些服务。这使得您的程序架构非常简单且松耦合,因为您只需要考虑如何监听全局对象模型中的变化以及如何更新该对象模型。您永远不会考虑,嗯……当用户在 StudentProfile
UserControl 内的“First Name”文本框中键入内容时,需要更改标题栏。嗯……用户控件在另一个 DLL 中。我如何捕获文本框中的更改通知并将其一直传递到顶层的 Form
。嗯……看起来我们需要在用户控件上公开一个公共事件,并且我们需要从 Form
中以某种方式订阅它。现在从 Form
中,我如何获得对用户控件的引用,该用户控件托管在另一个控件中,该控件又托管在另一个控件中,而且还托管在另一个控件中。嗯……所以,我们需要为所有这些嵌套的用户控件添加事件,以便将深埋在 StudentProfile
控件中的事件冒泡上去。生活多么愉快,不是吗?
与执行这些操作不同,您可以让 Form
订阅 StudentCollection
的 OnChange
事件。StudentProfile
控件将 UI 的更改反映到 Student
对象并触发事件。Form
将立即捕获它并更新标题栏。但这需要您拥有一个全局对象模型,就像在 MS Word 的对象模型中看到的那样。
让我们看看我们通常如何设计桌面应用程序,以及自动化模型如何极大地改变我们的开发方式。

当我拥有我多年来、项目一个接一个项目地完善的 ItemBase
类时,创建这样一个对象模型就相当容易了。您所需要做的就是扩展它,在构造函数中将父级传递给基类,并在您需要时调用基类上的方法。例如,StudentModel
类就像它看起来一样简单。
public class StudentModel : ItemBase
{
public StudentModel( Student profile, object parent ) : base( parent )
{
this.Profile = profile;
profile.AfterChanged += new StudentEventHandler(profile_AfterChanged);
}
void profile_AfterChanged(object sender, StudentEventArgs e)
{
base.IsDirty = true;
base.NotifyChange("Dirty");
}
}
同样,有一个 CollectionBase
类,它是所有可观察集合的基础。此类提供了两个主要功能:
- 项的通知:添加、删除、清除。
- 来自任何包含项的通知。
第二个功能最有用了。如果您有一个包含 1000 个 Student
对象的 StudentCollection
类,您不能仅仅订阅 1000 个对象的 OnChange
事件。相反,您订阅集合的 OnItemChange
事件,该事件在子对象触发其 OnChange
事件时自动触发。CollectionBase
类为您处理了来自其子对象的事件订阅和传播的所有困难。
public abstract class CollectionBase : System.Collections.CollectionBase
{
public event ItemChangeHandler OnItemChange;
public event ItemDetachHandler OnItemDetach;
public event ItemShowHandler OnItemShow;
public event ItemSelectHandler OnItemSelect;
public event ItemUndoStateHandler OnItemUndo;
public event ItemRedoStateHandler OnItemRedo;
public event ItemBeginUpdateHandler OnItemBeginUpdate;
public event ItemEndUpdateHandler OnItemEndUpdate;
public event CollectionAddHandler OnItemCollectionAdd;
public event CollectionRemoveHandler OnItemCollectionRemove;
public event CollectionClearHandler OnItemCollectionClear;
public event SelectionChangeHandler OnItemCollectionSelectionChanged;
public event CollectionAddRangeHandler OnAddRange;
public event CollectionRefreshHandler OnRefresh;
}
ItemBase
提供了另一个功能,即监听子集合。例如,一个 Course
对象包含一个 Section
对象集合,该集合又包含一个 SectionRoutine
对象集合。Course
对象(作为 ItemBase
)可以接收来自继承 CollectionBase
的 SectionCollection
触发的事件。结果,Course
对象可以接收来自 SectionCollection
中包含的 Section
对象的事件。
命令模式
对于桌面应用程序,这可能是所有模式中最好的。命令模式有许多变体,但最简单的形式如下所示:

我已将命令模式用于所有涉及 Web 服务调用和 UI 更新的活动。以下是我使用过的命令列表,它将为您提供如何将代码分解为小命令的思路:
LoadAllCourseCommand
- 通过调用 Course Web 服务加载所有课程和节,并填充_Application.Courses
集合。AuthenticateCommand
- 调用 Security Web 服务以验证指定的凭据。LoadStudentDetailCommand
- 加载特定学生的详细信息。加载学生进行编辑时调用。SaveStudentCommand
- 由“发送/接收”模块调用以将学生信息保存到服务器。CloseAllDocumentsCommand
- 由 Window->Close All 菜单项调用以关闭所有打开的文档。CloseCurrentDocumentCommand
- 由 Window->Close 菜单项和 Tab->Close 菜单项调用以关闭当前文档。ExitCommand
- 退出应用程序。
因此,您可以看到,所有用户活动都转换为某种形式的命令。命令就像小指令包。通常每个命令都是自给自足的。它执行所有活动,如调用 Web 服务、获取数据、更新对象模型以及选择性地更新 UI。这使得命令真正可重用,并且非常容易从任何地方调用。它还减少了提供类似功能的模块之间的重复代码。
有关命令模式的详细信息,请参阅我著名的文章:
后台发送接收任务
现在我们将回顾一下我们到目前为止所见过的所有内容的完整实现。让我们看看复杂的“发送接收”模块,它执行同步。

有两个类承载此视图:
SendReceiveDocument
- 一个继承自DocumentBase
的类,作为对象模型和提供 UI 的用户控件之间的网关。SendReceiveUI
- 一个提供交互性的UserControl
。
SendReceiveDocument
非常简单。
public class SendReceiveDocument : DocumentBase
{
private IList<StudentModel> _Students;
public IList<StudentModel> Students
{
get { return _Students; }
set { _Students = value; }
}
public SendReceiveDocument(IList<StudentModel> students, object parent)
: base("SendReceive", "Send/Receive", new SendReceiveUI(), parent)
{
this._Students = students;
}
public override bool Accept(object data)
{
if (data is IList<StudentModel>)
return base.UI.AcceptData(data);
else
return false;
}
public override bool CanAccept(object data)
{
return false;
}
public override bool IsFeatureSupported(object feature)
{
return false;
}
}
它包含一个需要同步的 Student
列表。SendReceiveUI
继承自 DocumentUIBase
。这是一个用于所有 Document 类型用户控件的基用户控件。DocumentUIBase
提供了一个对它需要显示的 Document
的引用。
public partial class SendReceiveUI : DocumentUIBase
{
/// <summary>
/// Return the students list from the document
/// </summary>
private IList<StudentModel> _Students
{
get { return (base.Document as SendReceiveDocument).Students; }
set { (base.Document as SendReceiveDocument).Students = value; }
}
public SendReceiveUI()
{
InitializeComponent();
}
internal override bool AcceptData(object data)
{
this._Students = data as IList<StudentModel>;
this.StartSendReceive();
return true;
}
}
因此,架构如下:

同步开始时,SendReceiveUI
有一个名为 StartSendReceive
的方法被调用。
private void StartSendReceive()
{
// Populate the listview with students
this.tasksListView.Items.Clear();
foreach (StudentModel student in this._Students)
{
ListViewItem item = new StudentListViewItem(student);
this.tasksListView.Items.Add(item);
}
...
...
// Start background synchronization
worker.RunWorkerAsync();
}
这里 worker
是一个 BackgroundWorker
组件,它是 .NET 2.0 的一项绝佳功能。该组件提供了一种非常方便的方式来在 UI 中提供多线程。当您将此控件放入 Form
或 UserControl
时,您会获得三个事件:
DoWork
- 在此处执行实际工作。此方法在不同的线程中执行,您无法从此事件或任何源自此事件的调用中的任何函数访问任何 UI 元素。ProgressChanged
- 当您在 worker 组件上调用ReportProgress
方法时,将触发此事件。此事件被流式传输到 UI 线程。因此,您可以自由使用 UI 组件。但是,您在此处所做的操作会阻塞主 UI 线程。因此,您需要在此处执行非常少量的工作。同时请记住不要过于频繁地调用ReportProgress
。RunWorkerCompleted
- 在DoWork
事件中的执行完成后触发。此函数从 UI 线程调用,因此您可以自由使用 UI 控件。
这是 DoWorker
方法的定义:
private void worker_DoWork(object sender, DoWorkEventArgs e)
{
// This method will run on a thread other than the UI thread.
// Be sure not to manipulate any Windows Forms controls created
// on the UI thread from this method.
this.BackgroundThreadWork();
}
我通常遵循在从此处调用的方法中包含“Background”一词的实践。这给了我一个视觉提示,让我记住我不在 UI 线程中。此外,从此类函数调用的任何其他函数也标有“Background”一词。
实际工作在此方法中执行。让我们看看 Send/Receive 模块做了什么:
private void BackgroundThreadWork()
{
foreach (StudentModel model in this._Students)
{
// Do the synchronization
try
{
// 1. Send changes
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Sending, model);
ICommand saveCommand = new SaveStudentCommand(model.Profile);
saveCommand.Execute();
// 2. Receive changes
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Receiving, model);
ICommand loadCommand = new
LoadStudentDetailCommand(model.Profile.ID);
loadCommand.Execute();
// 3. Complete
model.IsDirty = false;
model.NotifyChange("Reloaded");
model.LastErrorInSave = string.Empty;
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Completed, model);
}
catch (Exception x)
{
model.LastErrorInSave = x.Message;
worker.ReportProgress((int)
StudentListViewItem.StatusEnum.Error, model);
}
}
}
在此方法中,我们调用 Web 服务来执行实际工作。由于此方法在后台线程中运行,因此在 Web 服务调用进行期间 UI 不会阻塞。
您可以看到,任务实际上被封装在小的命令中。这些命令执行 Web 服务调用并更新对象模型。我不需要从这里担心刷新 UI 或重新填充学生列表下拉菜单,因为每当对象模型更新时,它们都会自动更新。我根本不需要考虑应用程序的任何其他部分,而可以专注于执行与此上下文相关的职责。
因此,您可以看到,每当您向应用程序添加新模块时,您都不必担心更改现有模块的代码以利用新模块。新模块只需订阅对象模型,并在触发某些事件时执行其工作。您也可以从应用程序的任何部分拆下模块,而无需担心其对其他模块的依赖性。没有依赖性。每个人都只依赖于对象模型。每个人都在监听对象模型以达到自己的目的。没有人关心谁进来,谁离开。
这是支持自动化对象模型的真正强大功能。
所有者绘制的 ListBox
您在“发送接收”模块的屏幕截图中看到,一个简单的 ListBox
同时显示了图标和文本。如果告诉 ListBox
不要自己进行任何渲染,而是让您负责渲染,则这是可能的。这称为所有者绘制。.NET 2.0 的 ListBox
控件具有 DrawMode
属性,该属性可以具有以下枚举值:
Normal
- 框架执行列表框项的默认渲染,即纯文本。OwnerDrawFixed
- 您可以绘制项,但每个项的高度是固定的。OwnerDrawVariable
- 您可以决定每个项的高度,并根据需要绘制每个项。
设置绘制模式后,订阅 DrawItem
事件并执行绘制。
private void errorsListBox_DrawItem(object sender, DrawItemEventArgs e)
{
Rectangle bounds = e.Bounds;
bounds.Inflate(-4, -2);
float left = bounds.Left;
e.Graphics.FillRectangle(Brushes.White, e.Bounds);
if (e.State == DrawItemState.Focus
|| e.State == DrawItemState.Selected)
ControlPaint.DrawFocusRectangle(e.Graphics, e.Bounds);
e.Graphics.DrawImage(this.imageList1.Images[3],
new PointF(left, bounds.Top));
left += this.imageList1.Images[3].Width;
RectangleF textBounds = new RectangleF( left, bounds.Top,
bounds.Width, bounds.Height);
e.Graphics.DrawString(this.errorsListBox.Items[e.Index] as string,
this.errorsListBox.Font, Brushes.Black, textBounds);
e.Graphics.DrawLine(Pens.LightGray, e.Bounds.Left, e.Bounds.Bottom,
e.Bounds.Right, e.Bounds.Bottom);
}
您将从参数 e
中获得关于绘制上下文的所有必要信息,它允许您获取当前项的 Graphics
和 Bounds
。您可以在代码中看到,在边界内,我们可以使用 Graphics
类绘制我们想要的任何内容。首先,我们用白色填充该区域,以清除该区域。然后,我们检查这是否是当前项,并相应地绘制焦点矩形。之后,我们绘制一个图像,后面跟着项的文本。
所有者绘制的 TreeView
就像所有者绘制的 首先,我们重写 |
![]() |
protected override void OnDrawNode(DrawTreeNodeEventArgs e)
{
// colors that depend if the row is currently selected or not,
// assign to a system brush so should not be disposed
// not selected colors
Brush brushBack = SystemBrushes.Window;
Brush brushText = SystemBrushes.WindowText;
Brush brushDim = Brushes.Gray;
if ((e.State & TreeNodeStates.Selected) != 0)
{
if ((e.State & TreeNodeStates.Focused) != 0)
{
// selected and has focus
brushBack = SystemBrushes.Highlight;
brushText = SystemBrushes.HighlightText;
brushDim = SystemBrushes.HighlightText;
}
else
{
// selected and does not have focus
brushBack = SystemBrushes.Control;
brushDim = SystemBrushes.WindowText;
}
}
RectangleF rc = new RectangleF(e.Bounds.X, e.Bounds.Y,
this.ClientRectangle.Width, e.Bounds.Height);
// background
e.Graphics.FillRectangle(brushBack, rc);
//base.OnDrawNode(e);
if (rc.Width > 0 && rc.Height > 0)
{
if (e.Node is CourseNode)
this.DrawCourseNode(e, rc, brushBack, brushText, brushDim);
else if (e.Node is SectionNode)
this.DrawSectionNode(e, rc, brushBack, brushText, brushDim);
else if (e.Node is RoutineNode)
this.DrawRoutineNode(e, rc, brushBack, brushText, brushDim);
}
}
这里的大部分代码都在处理当前节点状态以及绘制焦点矩形或灰色矩形。最后,根据当前节点对象,我们调用实际的渲染函数。
private void DrawCourseNode(DrawTreeNodeEventArgs e, RectangleF rc,
Brush brushBack, Brush brushText, Brush brushDim)
{
CourseNode node = e.Node as CourseNode;
if( (e.State & TreeNodeStates.Selected) == 0 )
{
this.GradientFill(e.Graphics, rc, Color.White,
Color.Gainsboro, rc.Height/2);
}
float textX = rc.Left; // +image.Width;
SizeF titleSize = this.DrawString(e.Graphics, node.Course.Title,
base.Font, brushText, textX, rc.Top);
float secondLineY = rc.Top + titleSize.Height;
if (!node.IsAllowed)
textX += this.DrawString(e.Graphics, "All Closed ",
base.Font, Brushes.DarkRed, textX, secondLineY ).Width;
int capacity = node.Capacity;
string count = "(" + capacity + ") ";
textX += this.DrawString( e.Graphics, count, base.Font,
brushText, textX, secondLineY ).Width;
// show number of sections
int sectioncount = node.Course.CourseSectionCollection.Count;
string sections = sectioncount.ToString() + " section"
+ (sectioncount>1?"s":"");
textX += this.DrawString(e.Graphics, sections,
base.Font, brushText, textX, secondLineY).Width;
}
在此方法中,我们拥有使用任何样式或字体绘制文本、图标的全部能力。这使我们能够完全控制 TreeView
控件的渲染,同时又完全重用了其所有功能。
Web 服务连接配置
当您向 Web 服务添加引用时,代理代码会在嵌入 Web 服务位置的代码中生成。因此,在开发过程中,如果您从本地计算机添加 Web 服务引用,您会看到 Web 服务引用设置为 localhost。因此,在投入生产之前,您需要更改所有 Web 服务代理,使其指向生产服务器。
另一个问题是,如果您的 Web 服务位置不是静态的呢?如果您需要从自己的配置文件中配置 URL,而不是在 Visual Studio 中进行硬编码,该怎么办?
我通过以下方式解决它:
- 我将服务器名称存储在配置变量中。
- 我使用
ServiceProxyFactory
类,它为我提供了所需的 Web 服务代理引用,而不是直接访问 Web 服务代理。
所以,基本上我遵循工厂模式,其中工厂决定返回哪个代理,它还在返回引用之前准备好该引用。例如:
internal class ServiceProxyFactory
{
/// <summary>
/// Contruct a student web service proxy ready to be called
/// </summary>
/// <returns></returns>
public static StudentService GetStudentService()
{
StudentService service = new StudentService();
service.Url = _Application.Settings.WebServiceUrl +
"StudentService.asmx";
SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader header =
new SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader();
header.Username = _Application.Settings.UserInfo.UserName;
header.PasswordHash = _Application.Settings.UserInfo.UserPassword;
header.VersionNo = _Application.Settings.VersionNo;
SetProxy(service);
service.CredentialSoapHeaderValue = header;
return service;
}
}
这里处理了以下问题:
- 根据配置设置代理的 URL。
- 设置一个包含凭据的 SOAP 头。更多信息稍后。
- 设置代理。配置决定是否使用代理。我需要将其配置为可选项,而不是选择默认代理,因为我想让我的 Web 服务器绕过默认代理。
服务器 - 服务面向架构
要求
根据维基百科,“服务”一词的定义如下:
一个独立的、无状态的函数,它通过一个明确定义的接口接受一个或多个请求并返回一个或多个响应。服务还可以执行离散的工作单元,例如编辑和处理事务。服务不依赖于其他函数或进程的状态。用于提供服务 Thus 的技术不构成此定义的一部分。
此外,服务需要是无状态的
不依赖于任何预先存在的条件。在 SOA 中,服务不依赖于任何其他服务 Thus 的状态。它们从请求中接收执行响应所需的所有信息。鉴于服务的无状态性,消费者可以将它们排序(编排)成多个序列(有时称为管道)来执行应用程序逻辑。
此外,SOA 是
与传统的面向对象架构不同,SOA 由松散耦合、高度可互操作的应用程序服务组成。由于这些服务可以通过不同的开发技术(如 Java 和 .NET)进行互操作,因此软件组件变得非常可重用。
SOA 提供了一种记录企业能力的方法和框架,并可以支持集成和整合活动。
示例 SOA

Web 项目(在源代码中可用)提供四项服务:
- Student Service:提供与学生相关的服务,例如加载所有学生、保存学生等。
- Course Service:提供与课程相关的服务,例如加载所有课程。
- Account Service:加载/保存账户,计算评估等。
- Security Service:使用 Enterprise Library 的 Authentication Provider 提供身份验证和授权。
Enterprise Library
Enterprise Library 是 Microsoft Patterns & Practices 应用程序块的一个主要新版本。应用程序块是可重用软件组件,旨在协助开发人员应对常见的企业开发挑战。Enterprise Library 将最常用的应用程序块的新版本整合到一个单一的集成下载包中。
Enterprise Library 的总体目标如下:
- 一致性。所有 Enterprise Library 应用程序块都具有一致的设计模式和实现方法。
- 可扩展性。所有应用程序块都包含定义的扩展点,允许开发人员通过添加自己的代码来自定义应用程序块的行为。
- 易用性。Enterprise Library 提供了许多可用性改进,包括图形配置工具、更简单的安装过程以及更清晰、更完整的文档和示例。
- 集成。Enterprise Library 应用程序块旨在协同工作,并经过测试以确保它们能够正常工作。也可以单独使用应用程序块(除非存在块相互依赖的情况,例如对 Configuration Application Block 的依赖)。
应用程序块有助于解决开发人员从一个项目到下一个项目经常面临的常见问题。它们被设计为封装 Microsoft 推荐的 .NET 应用程序最佳实践。它们可以快速轻松地添加到 .NET 应用程序中。例如,Data Access Application Block 通过易于使用的类提供对 ADO.NET 最常用功能的访问,从而提高了开发人员的生产力。它还解决了底层类库不支持的场景。(不同的应用程序有不同的需求,您不会发现每个应用程序块在您构建的每个应用程序中都有用。在使用应用程序块之前,您应该对您的应用程序需求以及应用程序块旨在解决的场景有充分的了解。)
(以上文本摘录自 Enterprise Library 文档。)
示例 Web 解决方案使用了以下应用程序块:
- Caching Application Block。缓存频繁访问的数据,如学生和课程列表。
- Configuration Application Block。配置所有其他应用程序块。
- Cryptography Application Block。加密/解密密码。
- Exception Handling Application Block。发布异常。
- Logging and Instrumentation Application Block。记录 Web 服务中的活动。
- Security Application Block。用于提供身份验证和授权。
安全
身份验证和授权由 Enterprise Library 的 Security Application Block 处理。这是一个功能丰富的块,包含以下内容:
- 基于数据库的身份验证和授权。表结构与 .NET 2.0 的 Membership Provider 相同。易于升级。
- 基于 Active Directory 的身份验证。
- 支持 Windows Authorization Manager 的授权。
它是一个很棒的块,可以在您的应用程序中使用。但是,如果您已经有一个自定义的基于数据库的身份验证和授权实现,您将需要进行以下操作才能将您自己编写的 A&A 代码替换为 Enterprise Library 提供的行业标准实践:
- 如果您有一个用户类,请从中提取密码字段。保留用户名字段。
- 将用户名字段映射到 Security Application Block 随附的数据库脚本创建的“Users”表。在您的用户表与 EL 的 Users 表的 User Name 列上创建外键。
- 更改您的身份验证代码以使用 EL 中可用的
AuthenticationProvider
。 - 用 EL 的基于角色的授权替换您自己编写的基于角色的授权。
这是身份验证的代码:
public static string Authenticate( string userName, string password )
{
IAuthenticationProvider authenticationProvider =
AuthenticationFactory.GetAuthenticationProvider(
AUTHENTICATION_PROVIDER);
IIdentity identity;
byte [] passwordBytes =
System.Text.Encoding.Unicode.GetBytes( password );
NamePasswordCredential credentials =
new NamePasswordCredential(userName, passwordBytes);
bool authenticated =
authenticationProvider.Authenticate(credentials, out identity);
if( authenticated )
{
ISecurityCacheProvider cache =
SecurityCacheFactory.GetSecurityCacheProvider(
CACHING_STORE_PROVIDER);
// Cache the identity. The SecurityCache will generate
// a token which is then returned to us.
IToken token = cache.SaveIdentity(identity);
return token.Value;
}
else
{
return null;
}
}
同样,对于授权,您可以检查用户是否属于某个特定角色:
public static bool IsInRole(string userName, string roleName)
{
IRolesProvider rolesProvider =
RolesFactory.GetRolesProvider(ROLE_PROVIDER);
IPrincipal principal =
rolesProvider.GetRoles(new GenericIdentity(userName));
if (principal != null)
{
bool isInRole = principal.IsInRole(roleName);
return isInRole;
}
else
{
return false;
}
}
但是仅检查用户的角色成员资格是不够的。在复杂的应用程序中,我们需要提供基于任务的授权,这意味着检查用户是否有权限执行特定任务。这可以通过以下方式完成:
public static bool IsAuthorized( string userName, string task )
{
IRolesProvider roleProvider =
RolesFactory.GetRolesProvider(ROLE_PROVIDER);
IIdentity identity = new GenericIdentity(userName);
IPrincipal principal = roleProvider.GetRoles(identity);
IAuthorizationProvider ruleProvider =
AuthorizationFactory.GetAuthorizationProvider(RULE_PROVIDER);
// Determine if user is authorized for the rule defined
// e.g. "Print Document"
bool authorized = ruleProvider.Authorize(principal, task);
return authorized;
}
源代码随附的 Security Console 应用程序允许您创建用户、创建角色,然后将用户分配给角色。稍后将详细介绍。
但是,定义规则有点不同。我的第一个期望是,规则应该在数据库中,以便可以从代码中更改它。结果却是静态的,并在应用程序配置文件中使用 Enterprise Library Configuration 应用程序定义。
在网络上传输密码
如果您不使用 HTTPS/SSL 或集成 Windows 身份验证,您将面临在网络上传输密码的问题。您不能以纯文本形式发送密码,因为任何人都可以窃听并获取密码。因此,您必须将密码的 MD5/SHA 哈希值发送到 Web 服务。最简单的方法是通过 SOAPHeader
传递凭据。这是一个名为 CredentialSoapHeader
的类,其中包含用户凭据:
public class CredentialSoapHeader : SoapHeader
{
/// <summary>
/// User name of the user
/// </summary>
public string Username;
/// <summary>
/// Hash of password, do not send password clear text over the wire
/// </summary>
public string PasswordHash;
/// <summary>
/// Version no of client. If it does not match with server's version no,
/// no request served
/// </summary>
public string VersionNo;
/// <summary>
/// Temporary security key generated for a particular client
/// When client
/// </summary>
public string SecurityKey;
}
您可以通过以下两种方式将身份验证信息传递给服务器:
- 始终在每次 Web 服务调用时发送用户名和密码哈希。
- 首次身份验证后,服务器将生成一个安全令牌,您可以使用该令牌进行后续调用。但是,这不安全,因为任何人都可以使用网络嗅探器捕获用户名和安全令牌,然后冒充。
在客户端,我们已经看到我们使用了一个工厂来生成 Web 服务代理,该代理准备好带有凭据的 Web 服务代理引用。
public static StudentService GetStudentService()
{
StudentService service = new StudentService();
service.Url = _Application.Settings.WebServiceUrl +
"StudentService.asmx";
SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader header =
new SmartInstitute.Automation.SmartInstituteServices.
StudentService.CredentialSoapHeader();
header.Username = _Application.Settings.UserInfo.UserName;
header.PasswordHash = _Application.Settings.UserInfo.UserPassword;
header.VersionNo = _Application.Settings.VersionNo;
}
现在我们**不**以纯文本形式发送从登录框收集的密码,而是发送密码的 MD5 哈希值。
UserInfo user = _Application.Settings.UserInfo;
user.UserName = this._UserName;
string passwordHash = HashStringMD5(this._Password);
user.UserPassword = passwordHash;
using (SecurityService service = ServiceProxyFactory.GetSecurityService())
{
user.SecurityKey = service.AuthenticateUser();
if (null != user.SecurityKey && user.SecurityKey.Length > 0)
{
this._IsSuccess = true;
}
}
从 Web 服务进行身份验证和授权
在服务器端,您需要转到每个应该从已身份验证的客户端调用的 Web 方法,并标记它们以检查凭据 SOAP 头。
[WebService(Namespace="http://oazabir.com/webservices/smartinstitute")]
public class StudentService : SecureWebService
{
[WebMethod]
[SoapHeader("Credentials")]
public Student GetStudentDetails( int userID )
{
base.VerifyCredentials();
return base.Facade.GetStudentDetail( userID );
}
[WebMethod]
[SoapHeader("Credentials")]
public StudentCollection GetAllStudents( )
{
base.VerifyCredentials();
return base.Facade.GetAllStudents();
}
[WebMethod]
[SoapHeader("Credentials")]
public bool SyncStudent( Student student )
{
base.VerifyCredentials();
return base.Facade.SyncStudent( student );
}
}
首先,我们指定调用方法时必须存在的 SoapHeader
。然后,我们调用基类的 VerifyCredentials
方法,该方法确保凭据是真实的。
名为 SecureWebService
的基类是一个简单的类,它继承自 .NET 框架的 WebService
类。它提供了所有 Web 服务所需的通用服务。
public class SecureWebService : WebService
{
public CredentialSoapHeader Credentials;
这个公共对象使用 SOAPHeader
的值进行填充。
protected Facade Facade = new Facade();
protected string VerifyCredentials()
{
System.Threading.Thread.Sleep(2000);
// For simulating delay in real service
if (this.Credentials == null
|| this.Credentials.Username == null
|| this.Credentials.PasswordHash == null)
{
throw new SoapException("No credential supplied",
SoapException.ClientFaultCode, "Security");
}
if( this.Credentials.VersionNo != Configuration.Instance.VersionNo )
throw new SoapException("The version you are using" +
" is not compatible with the server.",
SoapException.VersionMismatchFaultCode, "Security");
return CheckCredential(this.Credentials);
}
// authenticates a user's credentials passed in a custom SOAP header
private string CheckCredential( CredentialSoapHeader header )
{
// If security key provided, authenticate using the security key
if( null != header.SecurityKey && header.SecurityKey.Length > 0 )
{
if( Facade.Login( header.SecurityKey ) )
return header.SecurityKey;
else
throw new SoapException("Security key is not valid",
SoapException.ClientFaultCode, "Security" );
}
else
{
// Authenticate using credential
string key = Facade.Login( header.Username, header.PasswordHash );
if( null == key )
throw new SoapException("Invalid credential supplied",
SoapException.ClientFaultCode, "Security" );
else
return key;
}
}
}
强制从服务器进行更新
您在上述代码中可能注意到的一个有趣技巧是,我们在 SoapHeader
中传递了 VersionNo
。当您部署智能客户端时,如果您没有自动更新功能来强制用户在之前更新应用程序,您将遇到一个问题,即人们即使告诉他们,也不会在之前更新应用程序。结果,他们可能会使用旧代码,这会损坏服务器上的数据并为他人和他们自己带来问题。想象一下,您修复了一个复杂的账户计算,并要求所有会计人员获取最新版本。但有人忘记这样做了,并继续使用旧版本,这会导致账户报表计算错误。这对您来说是一场维护噩梦。
解决方案是强制拒绝任何未更新的客户端。在客户端应用程序中,存储一个包含版本号的配置变量。在服务器端,将允许的版本号存储在配置文件中。每个请求都包含 SOAPHeader
,其中包含 VersionNo
。将这些数字进行匹配,如果不匹配,则拒绝请求。这可以防止用户使用不受支持的客户端版本登录,甚至在已登录时也无法在服务器上执行任何操作。
if( this.Credentials.VersionNo != Configuration.Instance.VersionNo )
throw new SoapException("The version you are using" +
" is not compatible with the server.",
SoapException.VersionMismatchFaultCode, "Security");
配置 Security Application Block

开始之前,请确保您已将 Data Access Application Block 添加到配置中,并已正确映射到您的数据库。此数据库将在所有地方使用。
步骤 1:创建身份验证提供程序。我正在使用数据库提供程序。您可以使用其他选项,如 Active Directory Authentication Provider。请参阅 EL 文档了解详细信息。添加 Database Provider 后,选择存储身份验证信息的数据库。我使用 SmartInstitute 数据库来存储配置。
步骤 2:在 EL 的源代码中找到 SecurityDatabase.sql 文件。这是准备数据库的 SQL。如果您想使用默认的“Security”数据库,则可以运行它。但如果您想使用自己的数据库,请在编辑器中打开该文件并执行以下搜索和替换:
- 将 N'Security' 替换为 N'YourDatabaseName'
- 将 [Security] 替换为 [YourDatabaseName]
不要立即运行文件!它将删除您的数据库。从文件开头删除 DROP DATABASE
命令。其外观应如下所示:
IF NOT EXISTS (SELECT name FROM master.dbo.sysdatabases
WHERE name = N'SmartInstitute')
CREATE DATABASE [SmartInstitute]
COLLATE SQL_Latin1_General_CP1_CI_AS
GO
步骤 3:添加数据库角色提供程序。将其映射到同一数据库。
步骤 4:添加数据库配置文件提供程序。将其映射到同一数据库。
步骤 5:添加规则提供程序。这有点不同。右键单击提供程序名称并创建新规则。规则屏幕的外观如下:

在这里,您可以定义角色和标识符的静态表达式,这些表达式与规则匹配。如果用户及其角色匹配规则,则允许该用户访问规则。我们之前已经看到如何根据规则进行授权。
步骤 6. 添加安全缓存提供程序。我们将缓存凭据,以避免每次 Web 服务方法命中时都进行数据库调用以进行身份验证。
准备 Security Console 应用程序
Security Console 应用程序随 EL 的源代码一起提供。您需要修改 dataConfiguration.config 并定义自己的数据库名称,然后构建一个版本。
使用 EL 进行身份验证:意外情况
这是您需要使用的代码才能使用 EL 的 AuthenticationProvider
类进行身份验证:
IAuthenticationProvider authenticationProvider =
AuthenticationFactory.GetAuthenticationProvider(AUTHENTICATION_PROVIDER);
IIdentity identity;
byte [] passwordBytes = System.Text.Encoding.Unicode.GetBytes( password );
NamePasswordCredential credentials =
new NamePasswordCredential(userName, passwordBytes);
bool authenticated =
authenticationProvider.Authenticate(credentials, out identity);
这里的关键是:密码必须是纯文本。
但是您没有纯文本密码,因为客户端将发送密码的 MD5 哈希值。您也无法通过任何 AuthenticationProvider
函数从数据库中获取密码,以便您可以绕过此过程并直接匹配。因此,您别无选择,只能更改 EL 的代码,并陷入维护噩梦。
简单的解决方案是,您将密码的 MD5 哈希值原样传递给 AuthenticationProvider
,但在数据库中存储密码哈希值的哈希值。这意味着您在将密码存储到数据库之前会进行两次哈希处理。Security Console 应用程序创建用户帐户。因此,您可以轻松更改其代码以哈希密码的哈希值,然后将双重哈希字节存储在数据库中。因此,当您将 MD5 哈希值指定给 AuthenticationProvider
时,它会再次哈希它,然后与数据库中的字节进行匹配。数据库还包含哈希值的哈希值,并且它们匹配。
因此,我们将对 SecurityConsole 的代码进行一些修改,如下所示:
// First hash the password input using simple MD5. Use the same
// hashing method here which client will be using before sending
// password to Authenticate Method
string passwordHash = HashStringMD5( tbxPassword1.Text );
byte [] unicodeBytes = Encoding.Unicode.GetBytes(passwordHash);
byte[] password = hashProvider.CreateHash(unicodeBytes);
if (editMode)
{
userRoleMgr.ChangeUserPassword(tbxUser.Text, password);
}
这不会对服务器造成额外负载,因为第一次哈希由客户端完成,第二次哈希由服务器完成。
数据访问
Data Access Application Block
Enterprise Library 为您的所有数据库需求提供了一个方便的 Data Access Application Block。它有一个方便的 Database
类,提供了数据库访问所需的所有方法。然而,那些已经使用过以前的 Patterns & Practices Data Access Application Block 的人将面临主要问题,因为没有 SqlHelper
或 SqlHelperParameterCache
。这将迫使您检查所有数据访问代码,并将其更改为使用 Database
而不是 SqlHelper
。更糟糕的消息是,没有 SqlHelperParameterCache
。
这是一个真实的例子,您应该避免直接使用任何第三方代码,而是始终使用自己的包装器。无论它们看起来多么标准,来自多么可靠的来源,一旦您依赖于它们,而它们发生了变化,您就完蛋了。
由于这个问题,我创建了一个 SqlHelper
类,它的签名与以前的 DAB 相同。
public class SqlHelper
{
public static int ExecuteNonQuery( IDbTransaction transaction,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteNonQuery( transaction, spName, parameterValues );
}
public static int ExecuteNonQuery( string connectionString,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteNonQuery( spName, parameterValues );
}
public static SqlDataReader ExecuteReader( string connectionString,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteReader( spName, parameterValues ) as SqlDataReader;
}
public static SqlDataReader ExecuteReader(IDbTransaction transaction,
string spName, params object [] parameterValues )
{
Database db = DatabaseFactory.CreateDatabase();
return db.ExecuteReader( transaction, spName,
parameterValues ) as SqlDataReader;
}
}
一旦您创建了这样的类,您就可以保持现有代码不变,并使其与 EL 一起工作。但是,如果您以前使用过 SqlHelperParameterCache
,那么您也必须制作您自己的版本。
public class SqlHelperParameterCache
{
private static readonly Hashtable _ParamCache = new Hashtable();
public static IDataParameterCollection GetSpParameterSet( string
connectionString, string storedProcName )
{
Database db = DatabaseFactory.CreateDatabase();
if( !_ParamCache.ContainsKey( storedProcName ) )
{
// Open the connection to the database
SqlConnection connection = db.GetConnection() as SqlConnection;
if( connection.State != ConnectionState.Open )
connection.Open();
SqlCommand cmd = new SqlCommand( storedProcName, connection );
cmd.CommandType = CommandType.StoredProcedure;
SqlCommandBuilder.DeriveParameters( cmd );
_ParamCache.Add( storedProcName, cmd.Parameters );
connection.Close();
}
return _ParamCache[ storedProcName ] as IDataParameterCollection;
}
}
并发
离线工作会导致并发问题。这是不可避免的。因此,您需要使所有表都具有并发意识,并从代码或存储过程中处理并发。
这是我处理并发的方式:
- 在所有表中添加一个名为“
ChangeStamp
”的 Date 字段。 - 检索对象时,
ChangeStamp
会随对象一起传递。 - 当对象发送到数据库进行更新时,首先匹配对象的
ChangeStamp
与行的ChangeStamp
。如果匹配,则没有问题。 - 如果日期值不匹配,则表示有人在此期间修改了该行。因此,中止操作。
在存储过程中,这是代码:
UPDATE dbo.[Assessment]
SET
[ActivityFee] = @ActivityFee,
[AdmissionFee] = @AdmissionFee,
...
WHERE
[ID] = @ID
AND [ChangeStamp] = @ChangeStamp
这是代码如何处理并发的:
reader = SqlHelper.ExecuteReader(connectionString, "prc_Assessment_Update",
entity.ID,entity.ActivityFee, ... );
if (reader.RecordsAffected > 0)
{
RefreshEntity(reader, entity);
result = reader.RecordsAffected;
}
else
{
//must always close the connection
reader.Close();
// Concurrency exception
DBConcurrencyException conflict =
new DBConcurrencyException("Concurrency exception");
conflict.ModifiedRecord = entity;
AssessmentCollection dsrecord;
//Get record from Datasource
if (transactionManager != null)
dsrecord = AssessmentRepository.Current.GetByID(
this.transactionManager, entity.ID);
else
dsrecord = AssessmentRepository.Current.GetByID(connectionString,
entity.ID);
if(dsrecord.Count > 0)
conflict.DatasourceRecord = dsrecord[0];
throw conflict;
}
缓存
Caching Application Block 是我使用过程中没有遇到任何问题的唯一块。但同样,似乎 Cache Expiration 回调功能缺失了。
在服务器集群中缓存
当您拥有一个服务器集群时,您将遇到缓存问题。考虑以下场景:
- 您正在缓存 Web 方法中的对象列表。例如,在调用
GetAllStudents
后,我缓存所有学生。 - 现在,客户端修改了一个学生,我必须清除 Student 缓存,以便它再次从数据库加载并缓存。
- 集群中接收“SaveStudent”命令的 Web 服务器会清除自己的缓存。但它不会清除其他 Web 服务器的缓存。
- 客户端的下一个调用将转到另一个 Web 服务器,该服务器仍然保留缓存对象的旧副本。因此,即使在修改和保存学生后,客户端看到的仍然是学生的老信息。
解决此问题的唯一方法是使用集中的缓存存储。您将有一个专用的计算机来处理所有缓存的对象,这些对象从数据库加载成本高昂。但在大量负载下,这台单独的服务器可能会成为您的单点故障和性能瓶颈。
性能监控
EL 具有丰富的检测功能。它在应用程序块中使用 WMI 事件和性能计数器,让您有机会监控数据库调用的性能、身份验证和授权的成功/失败、衡量缓存命中率等。您可以使用这些性能计数器来检测服务器应用程序中的瓶颈。您可以使用 Windows 性能监视器查看 EL App Blocks 的运行情况。这是一个屏幕截图,显示了来自 EL App Blocks 的不同性能计数器。

使用 Code Smith 模板生成代码
Code Smith 是一个可以显著减少开发时间的工具。每天我们都会为数据库调用、对象填充、对象到 UI 填充以及反之编写通用代码。所有这些都可以通过 Code Smith 的一次鼠标点击自动生成。
除了 Facade 之外,整个服务器端代码都是使用出色的 .NET data tiers generator 生成的。然而,它使用旧的 Patterns & Practices Application Blocks 生成代码。修改这些模板并生成特定于 EL 的代码非常困难。在本文提供的源代码中,您会找到修改过的模板,该模板生成特定于 EL 的代码。
ClickOnce 部署
让我们来看看我最喜欢的 .NET 2.0 功能,ClickOnce 部署。这是桌面/智能客户端应用程序部署的真正革命性一步。部署和版本控制从未如此简单。我记得 C++ 时代我们曾经制作自己的手动编码的自定义安装程序和补丁的情况。在 VB 时代情况也没有太大变化。它引入了 DLL Hell。现在有了 .NET,DLL Hell 消失了。但是自动更新仍然无法从 .NET 框架中获得,您必须使用 Updater Application Block 等自定义库来提供自动更新。但现在有了 ClickOnce,一切都结束了。这是您能拥有的终极部署。
配置 ClickOnce 时,您需要在以下部分进行更改:

此对话框定义了您在已发布的网站上看到的文本。通过右键单击 EXE 项目并选择“Publish”发布应用程序后,您会看到类似这样的页面:

部署后,您可以自由地修改应用程序,添加新功能,修复 bug。完成这些后,右键单击 EXE 项目并再次单击“Publish”。所有用户在启动应用程序时都会收到更新版本。ClickOnce 负责下载最新版本并进行安装。

当然,对于那些不愿关闭应用程序的用户,他们不会获得更新的代码,当您想让他们更新时,您可以通过实现我之前介绍的 VersionNo 概念来阻止他们。
安装和运行示例应用程序
配置服务器

服务器使用 .NET 1.1 开发。因此,您将需要 Visual Studio 2003。
“Server”文件夹仅包含代码,数据库位于“Database”文件夹中。您需要按照以下步骤操作:
- 下载 Enterprise Library June 2005 并正确安装。安装后,转到 Start->Programs->Enterprise Library June 2005->Install Services。下载 URL。
- 从“Database”文件夹恢复数据库。使用数据库名称“SmartInstitute”。如果更改名称,则需要更改 dataconfiguration.config 并填写连接用户名、密码和数据库名称。
- 使用 IIS 管理器创建一个名为“SmartInstituteServices”的虚拟目录,该虚拟目录映射到“Server\SmartInstituteServices”。
- 打开“Server\SmartInstitute.sln”并进行构建。
- 修改“Server\SmartInstituteServices\dataconfiguration.config”并添加正确的数据库凭据。默认配置假定 SmartInstitute 数据库存在可信连接。将“ASPNET”(Windows XP)或“Network Services”(Windows 2003)帐户添加为 SmartInstitute 数据库的所有者。
您可以运行网站来查看一切是否正常工作。但是,由于缺少 CredentialSoapHeader
,因此将无法运行任何 Web 服务方法。
配置客户端
客户端使用 .NET 2.0 开发。因此,您将需要 Visual Studio 2005。
- 打开“client\SmartInstituteClient.sln”
- 转到 SmartInstitute.App -> Properties
- 转到 Signing 选项卡
- 单击“Create Test Certificate”
默认配置假定您的 Web 服务位于“localhost”。如果想更改,请转到 SmartInstitute.Automation 项目并打开“Properties\Settings.settings”。修改“WebServiceURL
”属性。
生成 Web 代理
每当您“添加引用”或“更新引用”Web 服务引用时,Visual Studio 都会在 Web 代理类(Reference.cs)中生成所有相关类。如果您打开代理的代码,您会看到,所有相关类,如 Student
、Course
、CourseSection
等,都是从 VS 从 WSDL 中发现的内容生成的。这是一个大问题,因为我们在 SmartInstitute.dll 中定义了所有域实体对象。它已经包含所有这些类,并且这个 DLL 在服务器和客户端之间共享。我找不到任何方法可以告诉 VS,请包含 SmartInstitute
命名空间,并且不要在 Web 代理类中生成重复类。结果是,每当您更新引用时,您都无法再构建项目。
如何解决这个问题
- 打开 Web 引用文件夹内的 Reference.cs 文件。通常您在 VS 中看不到它们。您需要通过单击“解决方案资源管理器”视图顶部的按钮来启用“显示所有文件”功能。
- 向下滚动直到找到
CredentialSoapHeader
类。 - 在此类之后,您将看到所有这些实体类都被创建了。删除所有这些类和枚举定义。
- 请记住不要删除代理中看到的委托和事件。那些是必需的。
我还没有找到自动执行此操作的解决方案。要么是我完全错过了 VS 中已经可以完成这项工作的任何功能,要么我需要编写一个可以自动完成这项工作的宏。
结论
我们已经涵盖了从 .NET 2.0 智能客户端的设计到开发、部署和维护。您还看到了如何在自己的项目中实现类似 Microsoft Office 应用程序的对象模型。这样一个对象模型会自动强制执行设计好的松耦合架构,从而提供终极的可扩展性。我们还看到了如何使用 XML Web 服务开发服务面向架构,以及 Enterprise Library 如何通过提供丰富的可重用组件集来显著降低开发成本和时间。示例代码将为您提供处理真实智能客户端开发障碍的充足示例,而可重用组件将大大缩短类似项目所需的时间。