使用自訂驗證組件庫擴充 Windows 表單(二)

來源:互聯網
上載者:User
本頁內容
回顧
程式性表單範圍驗證
ValidatorCollection
ValidatorManager
問題的另一面:更新 BaseValidator
枚舉 ValidatorCollection
聲明性表單範圍驗證:FormValidator
按照 定位順序驗證
我們所處的位置
致謝
Visual Basic .NET 與 C#
參考資料

回顧

上一篇,我們實現了一套驗證組件,這些組件藉助於固有的 Windows 表單驗證基礎結構,從 Visual Studio .NET Windows 表單設計器內部提供可重用的、聲明性的驗證。結果提供了針對每個控制項的驗證,即當使用者在控制項之間導航時發生的驗證。遺憾的是,當使用者完成資料輸入時,無法保證他們已經導航到並隨後驗證了表單中的所有控制項。在上述情形下,需要使用表單範圍的驗證解決方案來防止輸入不可靠的資料。在這一期中,我們將探討已有的自訂驗證組件庫如何以編程方式支援表單範圍驗證,然後再將其轉換為純聲明性的替代驗證。

程式性表單範圍驗證

實現表單範圍驗證的一種技術是在 Windows 表單的 OK 按鈕被單擊時,同時檢查所有相關控制項的有效性。讓我們使用上一期中的 Add New Employee 樣本表單( 1 所示)來說明這一方法。

圖 1 Add New Employee 表單及相關的驗證組件

每個驗證程式都公開了 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.");}

關於上述代碼,我們可以發表一些有意思的意見。首先,我的母親可以寫出比這更好的代碼。其次,它不是可伸縮的,因為隨著更多驗證程式被添加到表單中,該技術要求我們編寫更多的代碼。

返回頁首

ValidatorCollection

然而,最重要的意見是對每個驗證程式都反覆調用 ValidateIsValid。類似這樣的模式使人聯想到進行枚舉樣式的重構,它使我們可以編寫比我的母親更好的代碼,並且更為重要的是,可以提供可伸縮的替代代碼,這無疑會解決前面強調的兩個問題。遺憾的是,儘管 System.Windows.Forms.Form 確實通過 Controls 屬性實現了一個枚舉控制項集合,但它並沒有提供具有類似功能的組件。但有趣的是,Windows Forms Designer 的確將一個由設計器產生的組件集合插入到 Windows 表單中,該集合被恰當地稱為 components

public class AddNewEmployeeForm : System.Windows.Forms.Form {  ...  ///   /// Required designer variable.  ///   private System.ComponentModel.Container components = null;  ...}

components 管理一個組件列表,該列表中的組件利用非託管資源,並且需要在宿主表單被處置後處置這些資源。System.Windows.Forms.Timer 就是這樣的組件,它依賴於非託管 Win32 系統計時器(詳細討論超出了本文範圍,但可以在 Chris Sells 的著作 Windows Forms Programming in C# 的 Chapter 9 中找到)。因為 components 集合是由設計器管理的,並且因為我們的自訂驗證組件不依賴於非託管資源,所以我們不能使用這些組件來進行需要的枚舉。相反,我們必須建立自己的有保證的、強型別的 BaseValidators 集合。手動建立此類集合,尤其是強型別集合,可能是一件費時費力的苦差事。在上述情形下,我建議使用 CollectionGen (http://sellsbrothers.com/tools/#collectiongen),這是由 Chris Sells' et al 建立的 Visual Studio .NET 自訂工具,可為您完成這一繁重的工作。CollectionGen 為所需的 BaseValidator 集合(稱為 ValidatorCollection)產生以下代碼:

[Serializable]public class ValidatorCollection :   ICollection, IList, IEnumerable, ICloneable {  // CollectionGen implementation  ...}

或許得到的實現比我們解決特定問題所需的實現更為完整,但它所節省的數小時編碼時間使我能夠有空欣賞一些舊的 Knight Rider (http://www.imdb.com/title/tt0083437/) 情節。

返回頁首

ValidatorManager

遺憾的是,無論 Michael Knight 還是 K.I.T.T. 都不能為我們將 ValidatorCollection 合并到驗證庫中,因此我們需要關閉電視並自己動手完成。此時,我們確實有了可以枚舉的 ValidatorCollection,但要保證它含有所有正在啟動並執行驗證程式的列表,我們需要實現一種機制,以便在運行時在 ValidatorCollection 中添加和刪除所謂的驗證程式。我們建立了 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 都與特定表單相關聯,並且包含一個或多個對該表單所承載的 BaseValidators 的引用。關聯是在 BaseValidatorValidationManager 註冊和登出自己時建立的,因為 Register 方法和 DeRegister 方法都需要對 BaseValidator 以及承載它的表單的引用。特定表單的 ValidatorCollection 可以通過向 GetValidators 傳遞一個表單引用而檢索到。整個實現都是靜態(共用的),以保證記憶體中的訪問並且簡化用戶端代碼和 ValidatorManager 執行個體管理。

返回頁首

問題的另一面:更新 BaseValidator

RegisterDeRegister 需要在某個地方進行調用以使其全部有效,而這個地方就是 BaseValidator,因為此邏輯是所有驗證程式所共有的。由於 BaseValidator 與其宿主表單同生共死,因此需要將對 RegisterDeRegister 的調用與宿主表單的生存期進行同步,具體說來,這是通過處理宿主表單的 LoadClosed 事件實現的:

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);  }  ...}

下一步是將這些事件處理常式掛鈎到 LoadClosed 事件。我們需要的表單是 BaseValidatorControlToValidate 的宿主表單,並且因為 ControlToValidate 的類型是 Control,我們可以調用它的 FindForm 方法來檢索宿主表單。遺憾的是,我們不能從 BaseValidator 的建構函式中調用 FindForm,因為它的 ControlToValidate 在那時可能尚未被分配一個表單。這是 Windows Form Designer 使用 InitializeComponent 來儲存那些構建表單並向父容器分配控制項的代碼的結果:

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);  ...}

正如您所看到的,控制項執行個體的建立時間遠遠早於它被分配給表單的時間,後者又在關聯的驗證程式之後,這使得對 FindForm 的調用毫無用處。在此情況下,您可以求助於 System.ComponentModel.ISupportInitialize,它通過所定義的兩個方法(BeginInitEndInit)來解決與此類似的初始化依賴問題。Windows Forms Designer 使用反射來確定組件是否實現了 ISupportInitialize,如果是,則將對 BeginInitEndInit 的調用都插入到 InitializeComponent 中,分別在表單初始化之前和之後。因為能夠保證 EndInit 在已經為 BaseValidatorControlToValidate 分配一個父控制項之後調用,並且由此能夠從 FindForm 返回一個表單,所以這就是我們應該向 LoadClosed 事件進行註冊的地方。下面的代碼說明了實現方法:

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.EndInitDispose 的註冊和登出調用。因為 ValidatorManager 管理一個或多個由父表單進行雜湊運算的 ValidatorCollection,所以我希望確保每個 ValidatorCollection 都能在其關聯表單關閉時從 ValidatorManager 中刪除,而不是等待進行記憶體回收。

返回頁首

枚舉 ValidatorCollection

通過建立 ValidatorCollectionValidatorManager 以及更新 BaseValidator,可以完成為啟用需要的 BaseValidator 枚舉所需的註冊機制。圖 2 顯示了這些部分的結合方式的內部表示。

圖 2 ValidatorManager、ValidatorCollection 和 BaseValidator 的內部表示

要充分利用更新後的設計,我們只需要對 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

如果目標是編寫儘可能少的代碼,那麼我們可以通過將該解決方案重構為可重用性更好的模型將其進一步簡化。ASP.NET 本質上使用 System.Web.UI.Page(所有 ASP.NET 程式碼後置頁都派生於該類型)完成了這項工作。具體說來,Page 實現了下列面向驗證的成員:

public class Page : TemplateControl, IHttpHandler {  ...  public virtual void Validate();  public bool IsValid { get; }  public ValidatorCollection Validators { get; }  ...}

我們已經有了 ValidatorCollection(這樣命名是為了保持一致),而使用 ValidateIsValid 的結果與我們剛剛實現的基於表單範圍枚舉的驗證邏輯等效。遺憾的是,儘管 System.Windows.Forms.Form 實現了 Validate,但它與我們從自訂庫中利用的 Windows 表單本機驗證相聯絡,而不是與整合相聯絡。因此,繼續談論本文章系列中的主要話題之一是有意義的,即將適當的邏輯重新部署到開發人員可根據需要拖放到其表單上的可重用組件中。用於驗證表單的組件只能稱為 FormValidator,它實現了 ValidateIsValid,如下所示:

[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();    }    }}

除了實現 ValidateIsValid 以外,FormValidator 還實現了 HostingForm 屬性。因為與控制項從其 Parent 屬性或 FindForm 方法中確定宿主表單不同,組件本身無法確定自己的宿主表單,所以我們需要採取一點設計時技巧以實現同樣的目標。這一技巧顯示在 HostingForm 屬性中。魔術師永遠不會披露他的戲法,但我不是魔術師,而這也不是我的戲法,所以請自由地深入研究這一技術,並請參閱 Chris Sells' book 的 Chapter 9。在重建 CustomValidation 項目並將 FormValidator 組件添加到 Toolbox 後,我們可以簡單地將該組件拖動到表單上以便使用, 3 所示。

圖 3 使用 FormValidator 組件

藉助於 FormValidatorOK 按鈕的 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 顯示了運行時的結果。

圖 4 運行中的 FormValidator

儘管將用戶端代碼數量減少到三行已經不錯了,但如能減少到零行代碼可能會更好,尤其是在實現完全聲明性表單範圍驗證時。要達到此目標,FormValidator 需要實現自身與上述三行代碼對應的版本,並且在適當的時刻(當表單的 AcceptButton 被單擊時)為我們執行該版本。表單的 AcceptButtonCancelButton 都可以在設計時從 Property Browser 中設定, 5 所示。

圖 5 指定表單的 AcceptButton 和 CancelButton

這表示當使用者在表單上按 Enter 鍵時,指定的 AcceptButton單擊;而當使用者按 ESC 鍵時,指定的 CancelButton單擊FormValidator 需要確定其宿主表單的 AcceptButton,然後處理該按鈕的 Click 事件,該事件取決於從 InitializeComponent 內部設定的 AcceptButton。因此,我們必須重新實現 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.");  }  ...}
返回頁首

按照 定位順序驗證

對於使用者還會有用的一點是以可視方式處理驗證的順序。當前,FormValidator 按照可視順序選擇第一個無效控制項而不是第一個控制項,這與 定位順序所指定的一樣。圖 6 顯示了 Add New Employee 表單正確的 定位順序。

圖 6 指定 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 通過將焦點放在按 定位順序的第一個無效控制項上,顯示了結果。

圖 7. 將焦點放在按 定位順序的第一個無效控制項(即 Date of Birth)上。

下載樣本

我們所處的位置

在本期中,我們在第一期建立的針對每個控制項的驗證的基礎上,通過 FormValidator 提供了表單範圍的驗證。根據您使用強制回應對話方塊的方式,FormValidator 可支援完全聲明性表單範圍驗證。雖然如此,最終我們產生了兩個極端的驗證範圍:針對每個控制項表單範圍。然而,Windows 表單可能包含帶有數個選項卡的索引標籤控制項,其中每個選項卡鬆散相關或完全不相關,並且每個選項卡都需要其自身的驗證。這方面的例子有 Windows 案頭屬性對話方塊,它在每個屬性選項卡上都使用 Apply 按鈕。在上述方案中,容器特有的驗證會更有意義。在驗證主題文章系列中的下一期和最後一期中,我們將對該問題進行討論。我們還將擴充驗證組件庫,使其能夠通過基礎實現和可擴充的設計顯示驗證錯誤摘要,從而允許進一步自訂摘要解決方案 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.