Windows Forms中通過(guò)自定義組件實(shí)現(xiàn)統(tǒng)一的數(shù)據(jù)驗(yàn)證(一)
2007-04-18 21:30 by Anders Cui, 5446 visits, 網(wǎng)摘, 收藏, 編輯摘要
一直對(duì)WinForm中沒(méi)有像WebForm中那樣的驗(yàn)證控件耿耿于懷,,這幾天準(zhǔn)備開(kāi)發(fā)一套類(lèi)似的控件,。在網(wǎng)上找到大牛Michael Weinhardt的一個(gè)系列文章,寫(xiě)得非常棒,,所以基本上按他的思路下來(lái)的,。
在獲取用戶(hù)輸入及后續(xù)的處理過(guò)程中,數(shù)據(jù)校驗(yàn)是關(guān)鍵的一步,。本文將對(duì)Windows Forms中的校驗(yàn)機(jī)制進(jìn)行探討,,分析如何通過(guò)開(kāi)發(fā)自定義驗(yàn)證組件來(lái)提供更為高效的驗(yàn)證體驗(yàn)(類(lèi)似于ASP.NET中的驗(yàn)證控件)。
Windows Forms 驗(yàn)證機(jī)制介紹
簡(jiǎn)單地說(shuō),,驗(yàn)證是對(duì)數(shù)據(jù)進(jìn)行處理前確保其完整和正確的過(guò)程,。驗(yàn)證可以實(shí)現(xiàn)在數(shù)據(jù)層和業(yè)務(wù)規(guī)則層,而應(yīng)當(dāng)在表現(xiàn)層進(jìn)行前端的”保護(hù)”,。開(kāi)發(fā)人員通常在UI中為用戶(hù)提供友好的,、可交互的驗(yàn)證體驗(yàn),而要避免在N層應(yīng)用程序中進(jìn)行不必要的網(wǎng)絡(luò)間往返驗(yàn)證,。驗(yàn)證包含數(shù)據(jù)類(lèi)型,、范圍或業(yè)務(wù)規(guī)則等類(lèi)型,看下面這個(gè)簡(jiǎn)單的例子:
<!--[if !vml]-->
<!--[endif]-->
這個(gè)窗體中需要進(jìn)行下列驗(yàn)證:
- Name,,Date of Birth和Phone Number為必填項(xiàng)
- Date of Birth必須為正確的日期值
- Phone Number必須為正確的格式
- 新添加的雇員必須年滿(mǎn)18歲(杜絕童工)
要完成這些驗(yàn)證需要一個(gè)合適的機(jī)制,,Windows Forms已經(jīng)提供了一種,內(nèi)置在每個(gè)控件中,。要使控件支持驗(yàn)證,,須將它的CausesValidation 屬性設(shè)置為true,這也是所有控件的默認(rèn)值。如果控件的CausesValidation 屬性設(shè)置為true,,那么在它將焦點(diǎn)轉(zhuǎn)移到另一個(gè)控件(并且它的CausesValidation也為true)時(shí)會(huì)觸發(fā)Validating 事件,。因此,我們可以處理控件的Validating事件,,在這里實(shí)現(xiàn)驗(yàn)證邏輯,,像下面這樣:
{
if (txtName.Text.Trim().Length == 0)
{
e.Cancel = true;
return;
}
}
Validating 事件提供了CancelEventArgs 類(lèi)型的參數(shù),它的Cancel屬性使我們可以指定控件的值是否有效,。如果Cancel為true(即是無(wú)效的),,焦點(diǎn)仍然停留在無(wú)效的控件中;如果Cancel值為false(即通過(guò)了驗(yàn)證),,則會(huì)觸發(fā)Validated事件,,焦點(diǎn)也會(huì)轉(zhuǎn)移到新的控件。
現(xiàn)在,,責(zé)任落到了我們開(kāi)發(fā)人員這邊,,要以可視化的方式通知用戶(hù)數(shù)據(jù)是否有效,也許你想到的是狀態(tài)欄,,這種方式存在兩個(gè)問(wèn)題:
- 狀態(tài)欄只能每次顯式一條錯(cuò)誤信息,,即使窗體包含多個(gè)無(wú)效的控件輸入;
- 狀態(tài)欄離輸入控件”很遠(yuǎn)”,,很難確切指明哪個(gè)控件出現(xiàn)了錯(cuò)誤,。
此時(shí),ErrorProvider組件是更好的選擇:
{
errorProvider1.SetError(txtName, "Name is required.");
e.Cancel = true;
return;
}
errorProvider1.SetError(txtName, string.Empty);
CausesValidation、Validating和ErrorProvider提供了控件級(jí)驗(yàn)證的基礎(chǔ)機(jī)制,,我們可以用它們對(duì)控件逐一進(jìn)行驗(yàn)證,。
窗體級(jí)驗(yàn)證
Validating和ErrorProvider這對(duì)組合是一個(gè)不錯(cuò)的解決方案,可以在用戶(hù)輸入數(shù)據(jù)的時(shí)候進(jìn)行驗(yàn)證,。不幸的是,這種方法可能會(huì)使得我們無(wú)法進(jìn)行窗體級(jí)的驗(yàn)證,,而這在用戶(hù)點(diǎn)擊OK按鈕提交數(shù)據(jù)時(shí)顯然是必要的,,因?yàn)橛脩?hù)在點(diǎn)擊OK按鈕前,有些控件可能未曾獲得過(guò)焦點(diǎn),它們的控件級(jí)驗(yàn)證代碼也就不起作用了,。先看看窗體級(jí)驗(yàn)證的代碼:{
ctrl.Focus();
if (!Validate())
{
this.DialogResult = DialogResult.None;
return;
}
}
但Cancel按鈕就不需要實(shí)現(xiàn)窗體級(jí)的驗(yàn)證了,,它的工作往往是簡(jiǎn)單地將窗體關(guān)閉。但是現(xiàn)在,,如果當(dāng)前擁有焦點(diǎn)的控件數(shù)據(jù)是無(wú)效的,,Cancel按鈕將不能點(diǎn)擊,因?yàn)镃ancel按鈕的CausesValidation屬性默認(rèn)為true,,焦點(diǎn)會(huì)一直停留在無(wú)效的控件上,。我們只要將Cancel按鈕的CausesValidation屬性設(shè)置為false就好了。
注意:這里的窗體應(yīng)當(dāng)是模式窗體,,否則即使CausesValidation屬性設(shè)置為false,,也不能點(diǎn)擊。
至此,,使用數(shù)十行代碼,,我們的AddEmployee窗體就可以支持基本的驗(yàn)證了。編程式驗(yàn)證 vs. 聲明式驗(yàn)證
從生產(chǎn)力的角度來(lái)看,,上面的解決方案有一個(gè)根本的問(wèn)題:如果一個(gè)程序包含多個(gè)窗體,,而每個(gè)窗體又包含多個(gè)控件,那么將需要大量的用于驗(yàn)證的代碼,。這些代碼增大了UI的復(fù)雜性,,使得程序難以維護(hù),顯式是應(yīng)當(dāng)避免的,。一種方法是將那些通用的驗(yàn)證邏輯抽象為可重用的類(lèi)型,。有了這樣的類(lèi)型,還僅僅是第一步,,它仍需要編寫(xiě)代碼,。{TODO}我們需要這樣的解決方案:它具有Windows Forms UI的特點(diǎn),因此Windows Forms組件或控件是我們不錯(cuò)的選擇,。以這種方式封裝后,,開(kāi)發(fā)人員的工作就變成從工具箱上拖一個(gè)組件或控件放到窗體上,通過(guò)諸如屬性瀏覽器(Property Browser)這樣的設(shè)計(jì)期特性來(lái)配置它,,然后讓W(xué)indows Forms設(shè)計(jì)器幫我們將這些配置轉(zhuǎn)換為代碼,,這些代碼會(huì)出現(xiàn)在InitializeComponent方法中。這樣原來(lái)的編程式(programmatic)體驗(yàn)變成了聲明式(declarative)體驗(yàn),,而后者往往意味著高效,。
添加設(shè)計(jì)期支持
第一步是添加設(shè)計(jì)期的支持,如果我們的實(shí)現(xiàn)不需要UI支持,,可以從三種設(shè)計(jì)期組件繼承:System.ComponentModel.Component,, System.Windows.Forms.Control和 System.Windows.Forms.UserControl. Component,,否則可以繼承Control或UserControl。Control和UserControl的不同之處在于其呈現(xiàn)的方式,,前者需要編寫(xiě)代碼來(lái)呈現(xiàn)它,,而后者則通過(guò)其它控件或組件來(lái)呈現(xiàn)它。我們?cè)谇懊媸褂玫尿?yàn)證代碼沒(méi)有繪制任何內(nèi)容,,而是借助于ErrorProvider來(lái)提示用戶(hù),。因此,Component是我們最合適的選擇,。
Imitation Is the Sincerest Form of Flattery
下一步是要確定我們需要哪些種類(lèi)的驗(yàn)證組件,,可以參考一下ASP.NET中驗(yàn)證控件的實(shí)現(xiàn)機(jī)制。這樣能保持一定的一致性,,而且也不需要”重新發(fā)明輪子”了,。這樣那些ASP.NET的開(kāi)發(fā)人員也更容易上手。ASP.NET現(xiàn)在提供了如下的驗(yàn)證控件:
驗(yàn)證控件 |
描述 |
RequiredFieldValidator |
計(jì)算輸入控件的值以確保用戶(hù)輸入值,。 |
RegularExpressionValidator |
計(jì)算輸入控件的值,,以確定該值是否與某個(gè)正則表達(dá)式所定義的模式相匹配。 |
CompareValidator |
將由用戶(hù)輸入到輸入控件的值與輸入到其他輸入控件的值或常數(shù)值進(jìn)行比較,。 |
RangeValidator |
檢查輸入控件的值是否在指定的值范圍內(nèi),。 |
CustomValidator |
對(duì)輸入控件執(zhí)行用戶(hù)定義的驗(yàn)證。 |
同時(shí)我們還要考慮可擴(kuò)展性,,開(kāi)發(fā)人員在必要的時(shí)候可以較為容易地開(kāi)發(fā)自定義的驗(yàn)證組件,。最后,這個(gè)實(shí)現(xiàn)應(yīng)當(dāng)利用Windows Forms中已有的驗(yàn)證機(jī)制(前面提及的部分),。
引入RequiredFieldValidator
有了上面的設(shè)計(jì)思路,,現(xiàn)在要來(lái)點(diǎn)真的了。讓我們從最簡(jiǎn)單的驗(yàn)證情形開(kāi)始:RequiredFieldValidator,。
建立一個(gè)Component類(lèi),,名稱(chēng)為RequiredFieldValidator,其接口應(yīng)當(dāng)與ASP.NET中的對(duì)應(yīng)類(lèi)相同:
{
string ControlToValidate { get; set;}
string ErrorMessage { get; set;}
string InitialValue { get; set;}
bool IsValid { get; set;}
void Validate();
}
下面是每個(gè)成員的含義:
成員 |
描述 |
ControlToValidate |
指定要驗(yàn)證的控件 |
ErrorMessage |
控件未通過(guò)驗(yàn)證時(shí)顯式的信息,。 |
InitialValue |
某些情況下,,控件的默認(rèn)值用作提示,如”請(qǐng)選擇種類(lèi)”,,這時(shí)必填項(xiàng)意味著必須與默認(rèn)值不同,。此時(shí)用InitialValue。 |
IsValid |
在調(diào)用Validate方法后報(bào)告控件的數(shù)據(jù)是否有效,,默認(rèn)為true,。 |
Validate |
驗(yàn)證指定控件的值,并設(shè)置IsValid,。 |
在ASP.NET中,,ControlToValidate是字符串類(lèi)型的,,這種間接的做法在基于請(qǐng)求、無(wú)狀態(tài)的Web應(yīng)用程序中是必要的,。但在Windows Forms中我們則不必這么做,我們可以直接引用控件,。同時(shí),,我們要在內(nèi)部使用ErrorProvider組件,所以為其添加一個(gè)Icon屬性:
{
…
Control ControlToValidate { get; set;}
Icon Icon { get; set;}
…
}
好,,來(lái)看看具體的實(shí)現(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();
}
}
這種實(shí)現(xiàn)的關(guān)鍵在于如何掛接ControlValidate控件的Validating事件,,這種做法與前面的控件級(jí)驗(yàn)證相一致,還有一個(gè)額外的好處,,這里的ControlToValidate_Validating方法中,,沒(méi)有設(shè)置CancelEventArgs參數(shù)的Cancel屬性,這樣就不會(huì)把用戶(hù)困在一個(gè)控件中,。
組件的驗(yàn)證功能已經(jīng)實(shí)現(xiàn)了,,同時(shí)還為其添加了設(shè)計(jì)期支持。最終實(shí)現(xiàn)還提供了其它一些設(shè)計(jì)期特性:
- <!--[if !supportLists]-->指定了在屬性瀏覽器中設(shè)置ControlToValidate時(shí)可以選擇的控件種類(lèi),;
- 在屬性瀏覽器中隱藏了IsValid屬性,,因?yàn)樗沁\(yùn)行時(shí)的屬性。
編譯,,然后將組件添加到工具箱,。
讓我們回到前面的AddEmployee窗體,現(xiàn)在不再需要處理Validating事件了,,只要拖3個(gè)組件到窗體,,然后為它們?cè)O(shè)置屬性。
<!--[if !vml]-->
<!--[endif]-->
其中Phone Number域的驗(yàn)證組件的InitialValue為”Your number here.”,。怎么樣,,是不是很high?
BaseValidator:分而治之
實(shí)現(xiàn)了RequiredFieldValidator后,,其它類(lèi)型的驗(yàn)證組件應(yīng)當(dāng)比較容易實(shí)現(xiàn)了,。先別急,可沒(méi)你想的那么簡(jiǎn)單,。RequiredFieldValidator類(lèi)把特定的”必填”邏輯和其它對(duì)每個(gè)驗(yàn)證組件都適用的通用邏輯耦合在一起了,。這種情況下,應(yīng)當(dāng)把RequiredFieldValidator分解為兩個(gè)類(lèi)型:BaseValidator和減肥后的RequiredFieldValidator,。{
void Validate()
{
_isValid = EvaluateIsValid();
}
protected abstract bool EvaluateIsValid();
}
這樣定義的效果是,,BaseValidator必須通過(guò)繼承后才能使用,而EvaluateIsValid則必須實(shí)現(xiàn),。Validate方法通過(guò)EvaluateIsValid方法來(lái)設(shè)置IsValid,。這種技術(shù)也應(yīng)用在了ASP.NET的驗(yàn)證控件上,。
BaseValidator實(shí)現(xiàn)后,需要對(duì)RequiredFieldValidator進(jìn)行重構(gòu):
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);
}
}
更進(jìn)一步,,實(shí)現(xiàn)其它驗(yàn)證組件
通過(guò)使用基類(lèi)和派生類(lèi)將通用邏輯和特定邏輯分離后,,我們可以把注意力集中在特定的驗(yàn)證邏輯,這在RequiredFieldValidator中效果不錯(cuò),。下面會(huì)看到,,對(duì)于其它類(lèi)型的驗(yàn)證組件同樣很好,它們是:
- <!--[if !supportLists]-->RegularExpressionValidator
- CustomValidator
- CompareValidator
- RangeValidator
現(xiàn)在把它們一一實(shí)現(xiàn),。
RegularExpressionValidator
正則表達(dá)式是一種強(qiáng)大的文本模式匹配技術(shù),。如果文本域需要一定的模式,正則表達(dá)式無(wú)疑是很好的選擇,。
[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());
}
}
在設(shè)計(jì)時(shí),,開(kāi)發(fā)人員可以通過(guò)屬性瀏覽器提供用于驗(yàn)證的正則表達(dá)式。
CustomValidator
人生在世,,不如意者十有八九,。我們定義的驗(yàn)證組件不可能解決所有問(wèn)題,尤其是面對(duì)復(fù)雜的業(yè)務(wù)規(guī)則的時(shí)候,。這時(shí)只能編寫(xiě)自定義代碼,,CustomValidator 允許我們編寫(xiě)這些自定義代碼,同時(shí)仍能與其它的驗(yàn)證組件保持一致,,這在窗體級(jí)的統(tǒng)一驗(yàn)證過(guò)程中很重要,。CustomValidator 提供了Validating事件和ValidatingCancelEventArgs:
處理CustomValidator的Validating事件時(shí),只需在屬性瀏覽器中雙擊:然后,,只需添加合適的驗(yàn)證邏輯,,以確保新增的雇員不小于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歲,就會(huì)提示用戶(hù):
BaseCompareValidator
到目前為止,,我們的組件只能處理單個(gè)文本域的值,。但在某些情況下,驗(yàn)證過(guò)程可能涉及多個(gè)文本域或值,,比如確保文本域的值在兩個(gè)值之間(RangeValidator),;或比較兩個(gè)文本域的值是否相等(CompareValidator)。不管哪種情況,,我們都需要考慮類(lèi)型檢查,、轉(zhuǎn)換和比較等過(guò)程。這個(gè)功能應(yīng)當(dāng)封裝在一個(gè)新的類(lèi)型中:BaseCompareValidator,,而RangeValidator和CompareValidator則繼承自它,。ValidationDataType是一個(gè)自定義枚舉類(lèi)型,在何種數(shù)據(jù)類(lèi)型下進(jìn)行比較驗(yàn)證,。
RangeValidator
如果需要確??丶妮斎胫翟谥付ǖ姆秶鷥?nèi),,RangeValidator 可以滿(mǎn)足需要。它需要開(kāi)發(fā)人員指定最大值和最小值,,還有輸入值的數(shù)據(jù)類(lèi)型,。
<!--[if !vml]-->
<!--[endif]-->
CompareValidator
最后來(lái)看看CompareValidator,它用來(lái)進(jìn)行控件的等值測(cè)試,,可以與另一個(gè)控件的值或者指定的值進(jìn)行比較,。Operator屬性指定了比較操作的類(lèi)型,ControlToCompare和 ValueToCompare則指定了要比較的控件和指定值,。如果Operator屬性為DataTypeCheck,則還可以判斷控件的值是否為指定類(lèi)型,。
<!--[if !vml]-->
<!--[endif]-->
完整的自定義驗(yàn)證組件結(jié)構(gòu)
我們身在何處
示例代碼下載: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
本文版權(quán)歸作者和博客園共有,,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,,且在文章頁(yè)面明顯位置給出原文連接,,否則保留追究法律責(zé)任的權(quán)利。