Windows Forms中通過自定義組件實現(xiàn)統(tǒng)一的數(shù)據(jù)驗證(一)
2007-04-18 21:30 by Anders Cui, 5446 visits, 網(wǎng)摘, 收藏, 編輯摘要
一直對WinForm中沒有像WebForm中那樣的驗證控件耿耿于懷,這幾天準備開發(fā)一套類似的控件。在網(wǎng)上找到大牛Michael Weinhardt的一個系列文章,寫得非常棒,所以基本上按他的思路下來的。
在獲取用戶輸入及后續(xù)的處理過程中,數(shù)據(jù)校驗是關鍵的一步。本文將對Windows Forms中的校驗機制進行探討,分析如何通過開發(fā)自定義驗證組件來提供更為高效的驗證體驗(類似于ASP.NET中的驗證控件)。
Windows Forms 驗證機制介紹
簡單地說,驗證是對數(shù)據(jù)進行處理前確保其完整和正確的過程。驗證可以實現(xiàn)在數(shù)據(jù)層和業(yè)務規(guī)則層,而應當在表現(xiàn)層進行前端的”保護”。開發(fā)人員通常在UI中為用戶提供友好的、可交互的驗證體驗,而要避免在N層應用程序中進行不必要的網(wǎng)絡間往返驗證。驗證包含數(shù)據(jù)類型、范圍或業(yè)務規(guī)則等類型,看下面這個簡單的例子:
<!--[if !vml]-->
<!--[endif]-->
這個窗體中需要進行下列驗證:
- Name,Date of Birth和Phone Number為必填項
- Date of Birth必須為正確的日期值
- Phone Number必須為正確的格式
- 新添加的雇員必須年滿18歲(杜絕童工)
要完成這些驗證需要一個合適的機制,Windows Forms已經(jīng)提供了一種,內置在每個控件中。要使控件支持驗證,須將它的CausesValidation 屬性設置為true,這也是所有控件的默認值。如果控件的CausesValidation 屬性設置為true,那么在它將焦點轉移到另一個控件(并且它的CausesValidation也為true)時會觸發(fā)Validating 事件。因此,我們可以處理控件的Validating事件,在這里實現(xiàn)驗證邏輯,像下面這樣:
{
if (txtName.Text.Trim().Length == 0)
{
e.Cancel = true;
return;
}
}
Validating 事件提供了CancelEventArgs 類型的參數(shù),它的Cancel屬性使我們可以指定控件的值是否有效。如果Cancel為true(即是無效的),焦點仍然停留在無效的控件中;如果Cancel值為false(即通過了驗證),則會觸發(fā)Validated事件,焦點也會轉移到新的控件。
現(xiàn)在,責任落到了我們開發(fā)人員這邊,要以可視化的方式通知用戶數(shù)據(jù)是否有效,也許你想到的是狀態(tài)欄,這種方式存在兩個問題:
- 狀態(tài)欄只能每次顯式一條錯誤信息,即使窗體包含多個無效的控件輸入;
- 狀態(tài)欄離輸入控件”很遠”,很難確切指明哪個控件出現(xiàn)了錯誤。
此時,ErrorProvider組件是更好的選擇:
ErrorProvider組件的用法非常簡單,此處不再贅述,Validating事件的代碼如下:
{
errorProvider1.SetError(txtName, "Name is required.");
e.Cancel = true;
return;
}
errorProvider1.SetError(txtName, string.Empty);
CausesValidation、Validating和ErrorProvider提供了控件級驗證的基礎機制,我們可以用它們對控件逐一進行驗證。
窗體級驗證
Validating和ErrorProvider這對組合是一個不錯的解決方案,可以在用戶輸入數(shù)據(jù)的時候進行驗證。不幸的是,這種方法可能會使得我們無法進行窗體級的驗證,而這在用戶點擊OK按鈕提交數(shù)據(jù)時顯然是必要的,因為用戶在點擊OK按鈕前,有些控件可能未曾獲得過焦點,它們的控件級驗證代碼也就不起作用了。先看看窗體級驗證的代碼:{
ctrl.Focus();
if (!Validate())
{
this.DialogResult = DialogResult.None;
return;
}
}
但Cancel按鈕就不需要實現(xiàn)窗體級的驗證了,它的工作往往是簡單地將窗體關閉。但是現(xiàn)在,如果當前擁有焦點的控件數(shù)據(jù)是無效的,Cancel按鈕將不能點擊,因為Cancel按鈕的CausesValidation屬性默認為true,焦點會一直停留在無效的控件上。我們只要將Cancel按鈕的CausesValidation屬性設置為false就好了。
注意:這里的窗體應當是模式窗體,否則即使CausesValidation屬性設置為false,也不能點擊。
至此,使用數(shù)十行代碼,我們的AddEmployee窗體就可以支持基本的驗證了。編程式驗證 vs. 聲明式驗證
從生產(chǎn)力的角度來看,上面的解決方案有一個根本的問題:如果一個程序包含多個窗體,而每個窗體又包含多個控件,那么將需要大量的用于驗證的代碼。這些代碼增大了UI的復雜性,使得程序難以維護,顯式是應當避免的。一種方法是將那些通用的驗證邏輯抽象為可重用的類型。有了這樣的類型,還僅僅是第一步,它仍需要編寫代碼。{TODO}我們需要這樣的解決方案:它具有Windows Forms UI的特點,因此Windows Forms組件或控件是我們不錯的選擇。以這種方式封裝后,開發(fā)人員的工作就變成從工具箱上拖一個組件或控件放到窗體上,通過諸如屬性瀏覽器(Property Browser)這樣的設計期特性來配置它,然后讓Windows Forms設計器幫我們將這些配置轉換為代碼,這些代碼會出現(xiàn)在InitializeComponent方法中。這樣原來的編程式(programmatic)體驗變成了聲明式(declarative)體驗,而后者往往意味著高效。
添加設計期支持
第一步是添加設計期的支持,如果我們的實現(xiàn)不需要UI支持,可以從三種設計期組件繼承:System.ComponentModel.Component, System.Windows.Forms.Control和 System.Windows.Forms.UserControl. Component,否則可以繼承Control或UserControl。Control和UserControl的不同之處在于其呈現(xiàn)的方式,前者需要編寫代碼來呈現(xiàn)它,而后者則通過其它控件或組件來呈現(xiàn)它。我們在前面使用的驗證代碼沒有繪制任何內容,而是借助于ErrorProvider來提示用戶。因此,Component是我們最合適的選擇。
Imitation Is the Sincerest Form of Flattery
下一步是要確定我們需要哪些種類的驗證組件,可以參考一下ASP.NET中驗證控件的實現(xiàn)機制。這樣能保持一定的一致性,而且也不需要”重新發(fā)明輪子”了。這樣那些ASP.NET的開發(fā)人員也更容易上手。ASP.NET現(xiàn)在提供了如下的驗證控件:
驗證控件 | 描述 |
RequiredFieldValidator | 計算輸入控件的值以確保用戶輸入值。 |
RegularExpressionValidator | 計算輸入控件的值,以確定該值是否與某個正則表達式所定義的模式相匹配。 |
CompareValidator | 將由用戶輸入到輸入控件的值與輸入到其他輸入控件的值或常數(shù)值進行比較。 |
RangeValidator | 檢查輸入控件的值是否在指定的值范圍內。 |
CustomValidator | 對輸入控件執(zhí)行用戶定義的驗證。 |
同時我們還要考慮可擴展性,開發(fā)人員在必要的時候可以較為容易地開發(fā)自定義的驗證組件。最后,這個實現(xiàn)應當利用Windows Forms中已有的驗證機制(前面提及的部分)。
引入RequiredFieldValidator
有了上面的設計思路,現(xiàn)在要來點真的了。讓我們從最簡單的驗證情形開始:RequiredFieldValidator。
建立一個Component類,名稱為RequiredFieldValidator,其接口應當與ASP.NET中的對應類相同:
{
string ControlToValidate { get; set;}
string ErrorMessage { get; set;}
string InitialValue { get; set;}
bool IsValid { get; set;}
void Validate();
}
下面是每個成員的含義:
成員 | 描述 |
ControlToValidate | 指定要驗證的控件 |
ErrorMessage | 控件未通過驗證時顯式的信息。 |
InitialValue | 某些情況下,控件的默認值用作提示,如”請選擇種類”,這時必填項意味著必須與默認值不同。此時用InitialValue。 |
IsValid | 在調用Validate方法后報告控件的數(shù)據(jù)是否有效,默認為true。 |
Validate | 驗證指定控件的值,并設置IsValid。 |
在ASP.NET中,ControlToValidate是字符串類型的,這種間接的做法在基于請求、無狀態(tài)的Web應用程序中是必要的。但在Windows Forms中我們則不必這么做,我們可以直接引用控件。同時,我們要在內部使用ErrorProvider組件,所以為其添加一個Icon屬性:
{
…
Control ControlToValidate { get; set;}
Icon Icon { get; set;}
…
}
好,來看看具體的實現(xiàn)代碼:
{
Private Fields#region Private Fields
private Control controlToValidate = null;
private string errorMessage = string.Empty;
private string initialValue = string.Empty;
private bool isValid = true;
private Icon icon = new Icon(typeof(ErrorProvider), "Error.ico");
private ErrorProvider errorProvider = new ErrorProvider();
#endregion
Constructors#region Constructors
public RequiredFieldValidator()
{
InitializeComponent();
}
public RequiredFieldValidator(IContainer container)
{
container.Add(this);
InitializeComponent();
}
#endregion
Public Properties#region Public Properties
[Category("Behaviour")]
[Description("Get or sets the control to validate.")]
[DefaultValue(null)]
[TypeConverter(typeof(ValidatableControlConverter))]
public Control ControlToValidate
{
get
{
return controlToValidate;
}
set
{
controlToValidate = value;
if ((controlToValidate != null) && (!DesignMode))
{
controlToValidate.Validating += new CancelEventHandler(controlToValidate_Validating);
}
}
}
[Category("Appearance")]
[Description("Gets or sets the text for the error message.")]
[DefaultValue("")]
public string ErrorMessage
{
get
{
return errorMessage;
}
set
{
errorMessage = value;
}
}
[Category("Appearance")]
[Description("Gets or sets the Icon to display error message.")]
public Icon Icon
{
get
{
return icon;
}
set
{
icon = value;
}
}
[Category("Behaviour")]
[Description("Gets or sets the default value to validate against.")]
[DefaultValue("")]
public string InitialValue
{
get
{
return initialValue;
}
set
{
initialValue = value;
}
}
[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public bool IsValid
{
get
{
return isValid;
}
set
{
isValid = value;
}
}
#endregion
public void Validate()
{
if (controlToValidate == null)
{
isValid = true;
return;
}
string controlValue = controlToValidate.Text.Trim();
string _initValue;
if (initialValue == null)
{
_initValue = string.Empty;
}
else
{
_initValue = initialValue.Trim();
}
isValid = (controlValue != _initValue);
if (isValid)
{
errorProvider.SetError(controlToValidate, string.Empty);
}
else
{
errorProvider.SetError(controlToValidate, errorMessage);
}
}
private void controlToValidate_Validating(object sender, CancelEventArgs e)
{
Validate();
}
}
這種實現(xiàn)的關鍵在于如何掛接ControlValidate控件的Validating事件,這種做法與前面的控件級驗證相一致,還有一個額外的好處,這里的ControlToValidate_Validating方法中,沒有設置CancelEventArgs參數(shù)的Cancel屬性,這樣就不會把用戶困在一個控件中。
組件的驗證功能已經(jīng)實現(xiàn)了,同時還為其添加了設計期支持。最終實現(xiàn)還提供了其它一些設計期特性:
- <!--[if !supportLists]-->指定了在屬性瀏覽器中設置ControlToValidate時可以選擇的控件種類;
- 在屬性瀏覽器中隱藏了IsValid屬性,因為它是運行時的屬性。
編譯,然后將組件添加到工具箱。
讓我們回到前面的AddEmployee窗體,現(xiàn)在不再需要處理Validating事件了,只要拖3個組件到窗體,然后為它們設置屬性。
<!--[if !vml]-->
<!--[endif]-->
其中Phone Number域的驗證組件的InitialValue為”Your number here.”。怎么樣,是不是很high?
BaseValidator:分而治之
實現(xiàn)了RequiredFieldValidator后,其它類型的驗證組件應當比較容易實現(xiàn)了。先別急,可沒你想的那么簡單。RequiredFieldValidator類把特定的”必填”邏輯和其它對每個驗證組件都適用的通用邏輯耦合在一起了。這種情況下,應當把RequiredFieldValidator分解為兩個類型:BaseValidator和減肥后的RequiredFieldValidator。{
void Validate()
{
_isValid = EvaluateIsValid();
}
protected abstract bool EvaluateIsValid();
}
這樣定義的效果是,BaseValidator必須通過繼承后才能使用,而EvaluateIsValid則必須實現(xiàn)。Validate方法通過EvaluateIsValid方法來設置IsValid。這種技術也應用在了ASP.NET的驗證控件上。
BaseValidator實現(xiàn)后,需要對RequiredFieldValidator進行重構:
class RequiredFieldValidator : BaseValidator
{
string InitialValue {}
protected override bool EvaluateIsValid()
{
string controlValue = ControlToValidate.Text.Trim();
string initialValue;
if( _initialValue == null ) initialValue = "";
else initialValue = _initialValue.Trim();
return (controlValue != initialValue);
}
}
更進一步,實現(xiàn)其它驗證組件
通過使用基類和派生類將通用邏輯和特定邏輯分離后,我們可以把注意力集中在特定的驗證邏輯,這在RequiredFieldValidator中效果不錯。下面會看到,對于其它類型的驗證組件同樣很好,它們是:
- <!--[if !supportLists]-->RegularExpressionValidator
- CustomValidator
- CompareValidator
- RangeValidator
現(xiàn)在把它們一一實現(xiàn)。
RegularExpressionValidator
正則表達式是一種強大的文本模式匹配技術。如果文本域需要一定的模式,正則表達式無疑是很好的選擇。
[ToolboxBitmap(typeof(RegularExpressionValidator), "RegularExpressionValidator.ico")]
class RegularExpressionValidator : BaseValidator
{
string ValidationExpression {}
protected override bool EvaluateIsValid()
{
// Don't validate if empty
if( ControlToValidate.Text.Trim() == "" ) return true;
// Successful if match matches the entire text of ControlToValidate
string field = ControlToValidate.Text.Trim();
return Regex.IsMatch(field, _validationExpression.Trim());
}
}
在設計時,開發(fā)人員可以通過屬性瀏覽器提供用于驗證的正則表達式。
CustomValidator
人生在世,不如意者十有八九。我們定義的驗證組件不可能解決所有問題,尤其是面對復雜的業(yè)務規(guī)則的時候。這時只能編寫自定義代碼,CustomValidator 允許我們編寫這些自定義代碼,同時仍能與其它的驗證組件保持一致,這在窗體級的統(tǒng)一驗證過程中很重要。CustomValidator 提供了Validating事件和ValidatingCancelEventArgs:
處理CustomValidator的Validating事件時,只需在屬性瀏覽器中雙擊:然后,只需添加合適的驗證邏輯,以確保新增的雇員不小于18歲:
{
DateTime birth;
bool isDate = DateTime.TryParse(txtBirth.Text, out birth);
if (isDate)
{
DateTime legal = DateTime.Now.AddYears(-18);
e.Valid = (birth <= legal);
}
else
{
e.Valid = false;
}
}
如果小于18歲,就會提示用戶:
BaseCompareValidator
到目前為止,我們的組件只能處理單個文本域的值。但在某些情況下,驗證過程可能涉及多個文本域或值,比如確保文本域的值在兩個值之間(RangeValidator);或比較兩個文本域的值是否相等(CompareValidator)。不管哪種情況,我們都需要考慮類型檢查、轉換和比較等過程。這個功能應當封裝在一個新的類型中:BaseCompareValidator,而RangeValidator和CompareValidator則繼承自它。ValidationDataType是一個自定義枚舉類型,在何種數(shù)據(jù)類型下進行比較驗證。
RangeValidator
如果需要確??丶妮斎胫翟谥付ǖ姆秶鷥?,RangeValidator 可以滿足需要。它需要開發(fā)人員指定最大值和最小值,還有輸入值的數(shù)據(jù)類型。
<!--[if !vml]-->
<!--[endif]-->
CompareValidator
最后來看看CompareValidator,它用來進行控件的等值測試,可以與另一個控件的值或者指定的值進行比較。Operator屬性指定了比較操作的類型,ControlToCompare和 ValueToCompare則指定了要比較的控件和指定值。如果Operator屬性為DataTypeCheck,則還可以判斷控件的值是否為指定類型。
<!--[if !vml]-->
<!--[endif]-->
完整的自定義驗證組件結構
我們身在何處
示例代碼下載:CustomValidatorSample.rar
參考:
1. Extending Windows Forms with a Custom Validation Component Library. By Michael Weinhardt
2. Windows Forms Programming in C#. By Chris Sells
出處:http://anderslly.cnblogs.com
本文版權歸作者和博客園共有,歡迎轉載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責任的權利。