【MSDN文摘】使用自定义验证组件库扩展 Windows 窗体: Form Scope
使用自定義驗證組件庫擴展 Windows 窗體,第 2 部分(Windows 窗體探索)
發(fā)布日期: 5/28/2004 | 更新日期: 5/28/2004Michael Weinhardt
www.mikedub.net
2004 年 4 月 20 日
摘要:Michael Weinhardt 繼續(xù)撰寫其有關(guān)自定義驗證的系列文章,并對通過使用 FormValidator 組件來執(zhí)行窗體范圍的驗證進行了研究。(19 頁打印頁)
下載 winforms04202004_sample.msi 示例文件。
本頁內(nèi)容
| 回顧 | |
| 程序性窗體范圍驗證 | |
| ValidatorCollection | |
| ValidatorManager | |
| 問題的另一面:更新 BaseValidator | |
| 枚舉 ValidatorCollection | |
| 聲明性窗體范圍驗證:FormValidator | |
| 按照 Tab 鍵順序驗證 | |
| 我們所處的位置 | |
| 致謝 | |
| Visual Basic .NET 與 C# | |
| 參考資料 |
回顧
上 個月,我們實現(xiàn)了一套驗證組件,這些組件借助于固有的 Windows 窗體驗證基礎(chǔ)結(jié)構(gòu),從 Visual Studio .NET Windows 窗體設(shè)計器內(nèi)部提供可重用的、聲明性的驗證。結(jié)果提供了針對每個控件的驗證,即當用戶在控件之間導航時發(fā)生的驗證。遺憾的是,當用戶完成數(shù)據(jù)輸入時,無法 保證他們已經(jīng)導航到并隨后驗證了窗體中的所有控件。在上述情形下,需要使用窗體范圍的驗證解決方案來防止輸入不可靠的數(shù)據(jù)。在這一期中,我們將探討已有的 自定義驗證組件庫如何以編程方式支持窗體范圍驗證,然后再將其轉(zhuǎn)換為純聲明性的替代驗證。
返回頁首程序性窗體范圍驗證
實現(xiàn)窗體范圍驗證的一種技術(shù)是在 Windows 窗體的 OK 按鈕被單擊時,同時檢查所有相關(guān)控件的有效性。讓我們使用上一期中的 Add New Employee 示例窗體(如圖 1 所示)來說明這一方法。
圖 1 Add New Employee 窗體及相關(guān)的驗證組件
每個驗證程序都公開了 Validate 方法和 IsValid 屬性,它們都繼承自 BaseValidator。可以利用這些成員來確定窗體的有效性,如下所示:
private void btnOK_Click(object sender, System.EventArgs e) {// Validate all controls, including those whose Validating
// events may not have had the opportunity to fire
reqName.Validate();
reqDOB.Validate();
cstmDOB.Validate();
reqPhoneNumber.Validate();
rgxPhoneNumber.Validate();
reqTypingSpeed.Validate();
rngTypingSpeed.Validate();
reqCommences.Validate();
cmpCommences.Validate();
// Check whether the form is valid
if( (reqName.IsValid) &&
(reqDOB.IsValid) &&
(cstmDOB.IsValid) &&
(reqPhoneNumber.IsValid) &&
(rgxPhoneNumber.IsValid) &&
(reqTypingSpeed.IsValid) &&
(rngTypingSpeed.IsValid) &&
(reqCommences.IsValid) &&
(cmpCommences.IsValid) ) DialogResult = DialogResult.OK;
else MessageBox.Show("Form not valid.");
}
關(guān)于上述代碼,我們可以發(fā)表一些有意思的意見。首先,我的母親可以寫出比這更好的代碼。其次,它不是可伸縮的,因為隨著更多驗證程序被添加到窗體中,該技術(shù)要求我們編寫更多的代碼。
返回頁首ValidatorCollection
然而,最重要的意見是對每個驗證程序都反復調(diào)用 Validate 和 IsValid。類似這樣的模式使人聯(lián)想到進行枚舉樣式的重構(gòu),它使我們可以編寫比我的母親更好的代碼,并且更為重要的是,可以提供可伸縮的替代代碼,這無疑會解決前面強調(diào)的兩個問題。遺憾的是,盡管 System.Windows.Forms.Form 確實通過 Controls 屬性實現(xiàn)了一個枚舉控件集合,但它并沒有提供具有類似功能的組件。但有趣的是,Windows Forms Designer 的確將一個由設(shè)計器生成的組件集合插入到 Windows 窗體中,該集合被恰當?shù)胤Q為 components:
public class AddNewEmployeeForm : System.Windows.Forms.Form {...
///
/// Required designer variable.
///
private System.ComponentModel.Container components = null;
...
}
components 管理一個組件列表,該列表中的組件利用非托管資源,并且需要在宿主窗體被處置后處置這些資源。System.Windows.Forms.Timer 就是這樣的組件,它依賴于非托管 Win32 系統(tǒng)計時器(詳細討論超出了本文范圍,但可以在 Chris Sells 的著作 Windows Forms Programming in C# 的 Chapter 9 中找到)。因為 components 集合是由設(shè)計器管理的,并且因為我們的自定義驗證組件不依賴于非托管資源,所以我們不能使用這些組件來進行需要的枚舉。相反,我們必須創(chuàng)建自己的有保證的、強類型的 BaseValidators 集合。手動創(chuàng)建此類集合,尤其是強類型集合,可能是一件費時費力的苦差事。在上述情形下,我建議使用 CollectionGen (http://sellsbrothers.com/tools/#collectiongen),這是由 Chris Sells' et al 創(chuàng)建的 Visual Studio .NET 自定義工具,可為您完成這一繁重的工作。CollectionGen 為所需的 BaseValidator 集合(稱為 ValidatorCollection)產(chǎn)生以下代碼:
[Serializable]public class ValidatorCollection :
ICollection, IList, IEnumerable, ICloneable {
// CollectionGen implementation
...
}
或許得到的實現(xiàn)比我們解決特定問題所需的實現(xiàn)更為完整,但它所節(jié)省的數(shù)小時編碼時間使我能夠有空欣賞一些舊的 Knight Rider (http://www.imdb.com/title/tt0083437/) 情節(jié)。
返回頁首ValidatorManager
遺憾的是,無論 Michael Knight 還是 K.I.T.T. 都不能為我們將 ValidatorCollection 合并到驗證庫中,因此我們需要關(guān)閉電視并自己動手完成。此時,我們確實有了可以枚舉的 ValidatorCollection,但要保證它含有所有正在運行的驗證程序的列表,我們需要實現(xiàn)一種機制,以便在運行時在 ValidatorCollection 中添加和刪除所謂的驗證程序。我們創(chuàng)建了 ValidationManager 以滿足這一要求:
public class ValidatorManager {private static Hashtable _validators = new Hashtable();
public static void Register(BaseValidator validator, Form hostingForm) {
// Create form bucket if it doesn't exist
if( _validators[hostingForm] == null ) {
_validators[hostingForm] = new ValidatorCollection();
}
// Add this validator to the list of registered validators
ValidatorCollection validators =
(ValidatorCollection)_validators[hostingForm];
validators.Add(validator);
}
public static ValidatorCollection GetValidators(Form hostingForm) {
return (ValidatorCollection)_validators[hostingForm];
}
public static void DeRegister(BaseValidator validator,
Form hostingForm) {
// Remove this validator from the list of registered validators
ValidatorCollection validators =
(ValidatorCollection)_validators[hostingForm];
validators.Remove(validator);
// Remove form bucket if all validators on the form are de-registered
if( validators.Count == 0 ) _validators.Remove(hostingForm);
}
}
從根本上來說,ValidatorManager 使用 _validators 哈希表來管理由一個或多個 ValidatorCollection 實例組成的列表,其中每個實例表示特定窗體上承載的一組驗證程序。每個 ValidatorCollection 都與特定窗體相關(guān)聯(lián),并且包含一個或多個對該窗體所承載的 BaseValidators 的引用。關(guān)聯(lián)是在 BaseValidator 向 ValidationManager 注冊和注銷自己時建立的,因為 Register 方法和 DeRegister 方法都需要對 BaseValidator 以及承載它的窗體的引用。特定窗體的 ValidatorCollection 可以通過向 GetValidators 傳遞一個窗體引用而檢索到。整個實現(xiàn)都是靜態(tài)的(共享的),以保證內(nèi)存中的訪問并且簡化客戶端代碼和 ValidatorManager 實例管理。
返回頁首問題的另一面:更新 BaseValidator
Register 和 DeRegister 需要在某個地方進行調(diào)用以使其全部有效,而這個地方就是 BaseValidator,因為此邏輯是所有驗證程序所共有的。由于 BaseValidator 與其宿主窗體同生共死,因此需要將對 Register 和 DeRegister 的調(diào)用與宿主窗體的生存期進行同步,具體說來,這是通過處理宿主窗體的 Load 和 Closed 事件實現(xiàn)的:
public abstract class BaseValidator : Component {...
private void Form_Load(object sender, EventArgs e) {
// Register with ValidatorManager
ValidatorManager.Register(this, (Form)sender);
}
private void Form_Closed(object sender, EventArgs e) {
// DeRegister from ValidatorCollection
ValidatorManager.DeRegister(this, (Form)sender);
}
...
}
下一步是將這些事件處理程序掛鉤到 Load 和 Closed 事件。我們需要的窗體是 BaseValidator 的 ControlToValidate 的宿主窗體,并且因為 ControlToValidate 的類型是 Control,我們可以調(diào)用它的 FindForm 方法來檢索宿主窗體。遺憾的是,我們不能從 BaseValidator 的構(gòu)造函數(shù)中調(diào)用 FindForm,因為它的 ControlToValidate 在那時可能尚未被分配一個窗體。這是 Windows Form Designer 使用 InitializeComponent 來存儲那些構(gòu)建窗體并向父容器分配控件的代碼的結(jié)果:
private void InitializeComponent() {...
// Create control instance
this.txtDOB = new System.Windows.Forms.TextBox();
...
// Initialize control
//
// txtDOB
//
this.txtDOB.Location = new System.Drawing.Point(101, 37);
this.txtDOB.Name = "txtDOB";
this.txtDOB.Size = new System.Drawing.Size(167, 20);
this.txtDOB.TabIndex = 3;
this.txtDOB.Text = "";
...
//
// cstmDOB
//
this.cstmDOB.ControlToValidate = this.txtDOB;
this.cstmDOB.ErrorMessage = "Employee must be 18 years old";
this.cstmDOB.Icon =
((System.Drawing.Icon)(resources.GetObject("cstmDOB.Icon")));
this.cstmDOB.Validating +=
new CustomValidator.ValidatingEventHandler(this.cstmDOB_Validating);
//
// reqDOB
//
this.reqDOB.ControlToValidate = this.txtDOB;
this.reqDOB.ErrorMessage = "Date of Birth is required";
this.reqDOB.Icon =
((System.Drawing.Icon)(resources.GetObject("reqDOB.Icon")));
this.reqDOB.InitialValue = "";
...
//
// AddNewEmployeeForm
//
...
// Add control to form and set control's Parent to this form
this.Controls.Add(this.txtDOB);
...
}
正如您所看到的,控件實例的創(chuàng)建時間遠遠早于它被分配給窗體的時間,后者又在關(guān)聯(lián)的驗證程序之后,這使得對 FindForm 的調(diào)用毫無用處。在此情況下,您可以求助于 System.ComponentModel.ISupportInitialize,它通過所定義的兩個方法(BeginInit 和 EndInit)來解決與此類似的初始化依賴問題。Windows Forms Designer 使用反射來確定組件是否實現(xiàn)了 ISupportInitialize,如果是,則將對 BeginInit 和 EndInit 的調(diào)用都插入到 InitializeComponent 中,分別在窗體初始化之前和之后。因為能夠保證 EndInit 在已經(jīng)為 BaseValidator 的 ControlToValidate 分配一個父控件之后調(diào)用,并且由此能夠從 FindForm 返回一個窗體,所以這就是我們應該向 Load 和 Closed 事件進行注冊的地方。下面的代碼說明了實現(xiàn)方法:
public abstract class BaseValidator : Component, ISupportInitialize {...
#region ISupportInitialize
public void BeginInit() {}
public void EndInit() {
// Hook up ControlToValidate's parent form's Load and Closed events
// ...
Form host = _controlToValidate.FindForm();
if( (_controlToValidate != null) && (!DesignMode) &&
(host != null) ) {
host.Load += new EventHandler(Form_Load);
host.Closed += new EventHandler(Form_Closed);
}
}
#endregion
...
}
更新后的 InitializeComponent 如下所示:
private void InitializeComponent() {...
// Call BaseValidator implementation's BeginInit implementation
((System.ComponentModel.ISupportInitialize)(this.reqDOB)).BeginInit();
...
// Control, component and form initialization
...
// Call BaseValidator implementation's EndInit implementation
((System.ComponentModel.ISupportInitialize)(this.reqDOB)).EndInit();
}
您可能想知道我為什么沒有分別部署對 ISupportInitialize.EndInit 和 Dispose 的注冊和注銷調(diào)用。因為 ValidatorManager 管理一個或多個由父窗體進行哈希運算的 ValidatorCollection,所以我希望確保每個 ValidatorCollection 都能在其關(guān)聯(lián)窗體關(guān)閉時從 ValidatorManager 中刪除,而不是等待進行垃圾回收。
返回頁首枚舉 ValidatorCollection
通過創(chuàng)建 ValidatorCollection、ValidatorManager 以及更新 BaseValidator,可以完成為啟用需要的 BaseValidator 枚舉所需的注冊機制。圖 2 顯示了這些部分的結(jié)合方式的內(nèi)部表示。
圖 2 ValidatorManager、ValidatorCollection 和 BaseValidator 的內(nèi)部表示
要充分利用更新后的設(shè)計,我們只需要對 OK 按鈕的 Click 事件處理程序進行簡單更新:
private void btnOK_Click(object sender, System.EventArgs e) {// Better form wide validation
ValidatorCollection validators = ValidatorManager.GetValidators(this);
// Make sure all validate so UI visually reflects all validation issues
foreach( BaseValidator validator in validators ) {
validator.Validate();
}
foreach( BaseValidator validator in validators ) {
if( validator.IsValid == false ) {
MessageBox.Show("Form is invalid");
return;
}
}
DialogResult = DialogResult.OK;
}
由于我們不必隨著向窗體中添加更多的驗證程序而編寫越來越多的代碼,因此得到的代碼比我們最初編寫的代碼要精練得多,并且支持可伸縮性。這回,我們的代碼可不是我母親所能寫出來的。
返回頁首聲明性窗體范圍驗證:FormValidator
如果目標是編寫盡可能少的代碼,那么我們可以通過將該解決方案重構(gòu)為可重用性更好的模型將其進一步簡化。ASP.NET 本質(zhì)上使用 System.Web.UI.Page(所有 ASP.NET 代碼隱藏頁都派生于該類型)完成了這項工作。具體說來,Page 實現(xiàn)了下列面向驗證的成員:
public class Page : TemplateControl, IHttpHandler {...
public virtual void Validate();
public bool IsValid { get; }
public ValidatorCollection Validators { get; }
...
}
我們已經(jīng)有了 ValidatorCollection(這樣命名是為了保持一致),而使用 Validate 和 IsValid 的結(jié)果與我們剛剛實現(xiàn)的基于窗體范圍枚舉的驗證邏輯等效。遺憾的是,盡管 System.Windows.Forms.Form 實現(xiàn)了 Validate,但它與我們從自定義庫中利用的 Windows 窗體本機驗證相聯(lián)系,而不是與集成相聯(lián)系。因此,繼續(xù)談論本文章系列中的主要話題之一是有意義的,即將適當?shù)倪壿嬛匦虏渴鸬介_發(fā)人員可根據(jù)需要拖放到其窗體上的可重用組件中。用于驗證窗體的組件只能稱為 FormValidator,它實現(xiàn)了 Validate 和 IsValid,如下所示:
[ToolboxBitmap(typeof(FormValidator), "FormValidator.ico")]public class FormValidator : Component {
private Form _hostingForm = null;
...
public Form HostingForm {...}
public bool IsValid {
get {
// Get validators for this form, if any
ValidatorCollection validators =
ValidatorManager.GetValidators(_hostingForm);
if( validators == null ) return true;
// Check validity
foreach(BaseValidator validator in validators) {
if( validator.IsValid == false ) return false;
}
return true;
}
}
public void Validate() {
// Get validators for this form, if any
ValidatorCollection validators =
ValidatorManager.GetValidators(_hostingForm);
if( validators == null ) return;
// Validate
Control firstInTabOrder = null;
foreach(BaseValidator validator in validators) {
validator.Validate();
}
}
}
除了實現(xiàn) Validate 和 IsValid 以外,FormValidator 還實現(xiàn)了 HostingForm 屬性。因為與控件從其 Parent 屬性或 FindForm 方法中確定宿主窗體不同,組件本身無法確定自己的宿主窗體,所以我們需要采取一點設(shè)計時技巧以實現(xiàn)同樣的目標。這一技巧顯示在 HostingForm 屬性中。魔術(shù)師永遠不會披露他的戲法,但我不是魔術(shù)師,而這也不是我的戲法,所以請自由地深入研究這一技術(shù),并請參閱 Chris Sells' book 的 Chapter 9。在重新生成 CustomValidation 項目并將 FormValidator 組件添加到 Toolbox 后,我們可以簡單地將該組件拖動到窗體上以便使用,如圖 3 所示。
圖 3 使用 FormValidator 組件
借助于 FormValidator,OK 按鈕的 Click 事件處理程序被簡化為三行代碼:
private void btnOK_Click(object sender, System.EventArgs e) {formValidator.Validate();
if( formValidator.IsValid ) DialogResult = DialogResult.OK;
else MessageBox.Show("Form not valid.");
}
圖 4 顯示了運行時的結(jié)果。
圖 4 運行中的 FormValidator
盡管將客戶端代碼數(shù)量減少到三行已經(jīng)不錯了,但如能減少到零行代碼可能會更好,尤其是在實現(xiàn)完全聲明性窗體范圍驗證時。要達到此目標,FormValidator 需要實現(xiàn)自身與上述三行代碼對應的版本,并且在適當?shù)臅r刻(當窗體的 AcceptButton 被單擊時)為我們執(zhí)行該版本。窗體的 AcceptButton 和 CancelButton 都可以在設(shè)計時從 Property Browser 中設(shè)置,如圖 5 所示。
圖 5 指定窗體的 AcceptButton 和 CancelButton
這表示當用戶在窗體上按 Enter 鍵時,指定的 AcceptButton 被單擊;而當用戶按 ESC 鍵時,指定的 CancelButton 被單擊。FormValidator 需要確定其宿主窗體的 AcceptButton,然后處理該按鈕的 Click 事件,該事件取決于從 InitializeComponent 內(nèi)部設(shè)置的 AcceptButton。因此,我們必須重新實現(xiàn) ISupportInitialize,如下所示:
public class FormValidator : Component, ISupportInitialize {#region ISupportInitialize
public void BeginInit() {}
public void EndInit() {
if( (_hostingForm != null) ) {
Button acceptButton = (Button)_hostingForm.AcceptButton;
if( acceptButton != null ) {
acceptButton.Click += new EventHandler(AcceptButton_Click);
}
}
}
#endregion
private Form _hostingForm = null;
[Browsable(false)]
[DefaultValue(null)]
public Form HostingForm {...}
...
private void AcceptButton_Click(object sender, System.EventArgs e) {
Validate();
if( IsValid ) _hostingForm.DialogResult = DialogResult.OK;
else MessageBox.Show("Form not valid.");
}
...
}
返回頁首
按照 Tab 鍵順序驗證
對于用戶還會有用的一點是以可視方式處理驗證的順序。當前,FormValidator 按照可視順序選擇第一個無效控件而不是第一個控件,這與 Tab 鍵順序所指定的一樣。圖 6 顯示了 Add New Employee 窗體正確的 Tab 鍵順序。
圖 6 指定 Tab 順序
通過按 Tab 鍵順序進行驗證,用戶可以在窗體上從上到下來糾正無效字段,這要比看上去隨機的方法更直觀一些。要確保驗證按 Tab 鍵順序進行,必須按如下方式更新 FormValidator:
[ToolboxBitmap(typeof(FormValidator), "FormValidator.ico")]public class FormValidator : Component {
...
public Form HostingForm {...}
public bool IsValid {...}
public void Validate() {
// Validate all validators on this form, ensuring first invalid
// control (in tab order) is selected
Control firstInTabOrder = null;
ValidatorCollection validators =
ValidatorManager.GetValidators(_hostingForm);
foreach(BaseValidator validator in validators) {
// Validate control
validator.Validate();
// Record tab order if before current recorded tab order
if( !validator.IsValid ) {
if( (firstInTabOrder == null) ||
(firstInTabOrder.TabIndex >
validator.ControlToValidate.TabIndex) ) {
firstInTabOrder = validator.ControlToValidate;
}
}
}
// Select first invalid control in tab order, if any
if( firstInTabOrder != null ) firstInTabOrder.Focus();
}
}
圖 7 通過將焦點放在按 Tab 鍵順序的第一個無效控件上,顯示了結(jié)果。
圖 7. 將焦點放在按 Tab 鍵順序的第一個無效控件(即 Date of Birth)上。
返回頁首我們所處的位置
在本期中,我們在第一期建立的針對每個控件的驗證的基礎(chǔ)上,通過 FormValidator 提供了窗體范圍的驗證。根據(jù)您使用模式對話框的方式,FormValidator 可支持完全聲明性窗體范圍驗證。雖然如此,最終我們生成了兩個極端的驗證范圍:針對每個控件 和窗體范圍。然而,Windows 窗體可能包含帶有數(shù)個選項卡的選項卡控件,其中每個選項卡松散相關(guān)或完全不相關(guān),并且每個選項卡都需要其自身的驗證。這方面的例子有 Windows 桌面屬性對話框,它在每個屬性選項卡上都使用 Apply 按鈕。在上述方案中,容器特有的驗證會更有意義。在驗證主題文章系列中的下一期和最后一期中,我們將對該問題進行討論。我們還將擴展驗證組件庫,使其能夠通過基礎(chǔ)實現(xiàn)和可擴展的設(shè)計顯示驗證錯誤摘要,從而允許進一步自定義摘要解決方案。
返回頁首致謝
這一次要感謝很多人。首先,再次感謝 Ron Green 和 Chris Sells 對該問題進行的研究。感謝 Shawn A. Van Ness 和 Chris Sells et al 設(shè)計了 CollectionGen, 感謝 Stephen Goodwin 對文章簡介提供的意見,感謝 Ian Griffiths 和 Chris Sells 提供了宿主窗體設(shè)計時技巧,同時感謝為上一期提供反饋的讀者對我闡述的解決方案 I 所提的意見。我想只有當讀者愿意提出意見尤其是反對意見時:),我的文章才是真正有趣的。因此,歡迎將您的想法通過電子郵件發(fā)送給我 (mikedub@optusnet.com.au),尤其是在我可以將相關(guān)意見應用到這一專欄中時。
最后但絕非最不重要的是,我要向我的母親說一聲:母親節(jié)快樂,媽媽!
返回頁首Visual Basic .NET 與 C#
此 刻,可能沒有人過分關(guān)心我是用 Visual Basic .NET 還是用 C# 編寫代碼,因此我將繼續(xù)用 C# 編寫代碼。我將努力使其盡可能地易于閱讀,避免使用三元運算符,如一位讀者所要求的那樣。我希望同時提供 C# 和 Visual Basic .NET 代碼示例,但這需要大量的準備時間,尤其是本月的示例大約有 1300 行代碼。不過,如果有人能夠推薦一種良好的從 C# 到 Visual Basic .NET 的轉(zhuǎn)換程序,可以顯著減少轉(zhuǎn)換工作量,我將認真地考慮同時提供兩種語言的示例。
返回頁首參考資料
| ? | Windows Forms Programming in C#(作者:Chris Sells) |
| ? | CollectionGen (http://sellsbrothers.com/tools/#collectiongen) |
Michael Weinhardt 目前正專門從事各種 .NET 寫作活動,其中包括與 Chris Sells 共同創(chuàng)作《Windows Forms Programming in C#, 2nd Edition》(Addison Wesley) 以及撰寫本專欄。Michael 熱愛 .NET 的一切,尤其喜歡 Windows 窗體。他在業(yè)余時間喜歡觀看 80s Television Shows。詳細信息,請訪問 www.mikedub.net。
轉(zhuǎn)載于:https://www.cnblogs.com/Peter-Yung/archive/2007/03/16/677773.html
超強干貨來襲 云風專訪:近40年碼齡,通宵達旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的【MSDN文摘】使用自定义验证组件库扩展 Windows 窗体: Form Scope的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。