客户端/服务器端 ASP.NET 信用卡验证控件






4.82/5 (22投票s)
2004年2月29日
25分钟阅读

271586

4499
本文讨论了新版 ASP.NET 信用卡验证器,包括服务器端和客户端检查以及 VS.NET 设计器支持。
背景
本文承接了信用卡验证控件的先前版本。距离最初发布已经很长时间了,我承诺了更新版本,现在终于把它完成了,感觉很好。
该控件最初源于我正在从事的另一个项目——为DataCash的支付网关服务构建原生 .NET 接口——我构建了一个测试页面,并决定提供一些简单的信用卡号码验证也会很不错。
我制作了一个简单的 ASP.NET 验证控件,它使用了Luhn 公式——一个对所有数字位数进行校验和的算法。我稍后会介绍 Luhn 公式如何使用的基础知识,但更详细的信息可以在Webopedia找到。
随后,该控件进行了扩展,加入了卡片处理代码,以便开发人员控制接受哪些卡片。例如,有些商店可能不接受美国运通卡,因为其额外的处理费用;或者他们可能只接受借记卡付款。在该控件的最初几个版本中,卡片类型是通过在验证器的 ASP.NET 标签中设计时设置的属性来处理的。
我于2002年8月左右撰写了原始文章。从那时起,我收到了大量电子邮件,人们提出了改进建议,询问某些功能是否可能,或者他们如何围绕我的代码实现他们的代码。
毕业前,我还没有找到工作,所以开始思考如何既能让自己忙碌起来,又能开始赚钱。我致力于改进代码,根据建议添加功能等。当时市场上有一些其他的商业解决方案,但我觉得我计划和设计的这个更新版本会更好,提供更多的功能,并且开发人员集成到 ASP.NET 应用程序中所需的精力更少。
我现在已经开了一家公司,并将销售 .NET 组件,但我已经将更新后的代码发给了几位给我发邮件的人,并且也承诺了这次更新,所以将其作为商业产品发布是不对的。我可能会添加更多功能使其更全面,以便销售它,在这种情况下,那些对这个免费版本心存感激的人可以购买它 ;) 然而,我承诺不会撤销这篇或我在此贡献的任何其他文章。
该控件的演示将很快推出,在此之前,可以下载并运行演示。
解决方案目标
新解决方案的总体目标是提供一个更全面的产品。正如我所提到的,我最初打算将这个更新后的代码用于销售。我看到了一些其他提供信用卡验证的产品出现,但它们似乎只提供了非常基本的验证,并且需要开发人员付出相当大的努力才能将检查集成到页面中。我希望我的产品是一个即插即用的解决方案。然而,我最终决定转而将大部分功能实现并使其正常工作,然后发布代码(并最终发布这篇文章)。
我觉得以下是一些我应该添加到现有代码中的好功能:
- 提供客户端和服务器端验证
这使得在服务器往返之前可以进行更多的验证。对于连接速度慢的用户来说,这甚至更重要。
- Visual Studio .NET 设计器支持
尽管我倾向于使用简单的 HTML 编辑器编写页面,但提供与 Visual Studio .NET 的集成将使使用其所见即所得环境的开发人员更快地完成工作。
- 允许用户选择卡类型
尽管控件会检查卡类型是否有效/无效,但允许用户从下拉列表中选择类型将提供额外的检查级别,以确保号码输入正确。
- ASP.NET 页面之外的配置
这主要是为了将卡片类型从 ASP.NET 标签的属性中移除,允许应用程序中的任何 Web 表单使用相同的设置。如果接受了新的卡片类型,则只需在一个地方进行更改。
- 可扩展性
虽然各国只有少数几个大型卡片处理商(VISA、MasterCard、American Express),但还有其他的,人们给我发邮件询问是否可以添加任何单个卡片。我希望有一种方法可以让人们自己更新验证,添加对新卡片的支持而无需重新编译源代码。此外,如果商业化,我可以向现有客户提供任何更新,而无需他们重建代码或安装新的 DLL。
解决方案设计
我将首先介绍上述目标的通用设计影响,然后稍后深入讨论类设计等。
客户端验证
幸运的是,一位名叫 Chip Johansen 的先生给我发邮件说,他们已经为我在上一篇文章中使用的卡类型添加了客户端脚本支持。我仔细阅读了代码,发现它在了解如何实现检查方面非常有帮助。然而,它的 JavaScript 代码是针对卡类型硬编码的,这将阻碍我提供一个可扩展的解决方案。我应该补充一点,后来我还收到了 Peter Lange 的邮件,他提到他也以类似的方式添加了客户端支持。两组代码都有助于确定如何添加支持。
我查阅了各种 JavaScript 文章,特别是查看是否有办法能让服务器端使用的相同检查代码在客户端也使用。然后我发现 JavaScript 支持正则表达式。编写了一些基本的 JavaScript 代码后,我发现我可以在服务器端和客户端使用相同的正则表达式匹配代码。与现有代码的唯一改变是,更多的卡类型检查代码将通过正则表达式编写。
Visual Studio .NET 设计器支持
没什么好说的,添加支持本质上是一个实现问题。事实证明,这花了我相当长的时间才使其正常工作,在此过程中发现了许多 Visual Studio .NET 的小故障。
用户可选卡类型支持
CodeProject 上有一些帖子以及我收到的电子邮件,询问是否有办法在运行时更改验证器控件的属性,以允许修改接受的卡类型,从而允许用户从下拉列表中选择他们输入的卡类型。
显而易见的解决方案是在解决方案中添加一个派生自 `ListBox` 的附加控件。它将用表单将接受的卡类型列表自行填充。当用户提交其详细信息时,验证控件将检查此内容以识别应该输入的卡类型并对其进行检查。此外,这在客户端也应该起作用。
ASP.NET 标签之外的配置
同样,合乎逻辑的做法是在应用程序的配置文件 ( _web.config_ ) 中添加对控件配置的支持。实际上,将接受的卡片类型属性从 ASP.NET 控件的标签移到一个单独的 XML 配置文件中。
可扩展性
幸运的是,使用上述系统非常适合扩展性。创建一个新部分将允许 XML 配置文件包含所有接受的类型和用于验证号码布局的正则表达式。我收到了一封电子邮件,有人建议我将卡片类型移到一个可修改的集合中,定义一个新的 `CardType` 类型,允许开发人员添加自定义类型。这就是我采用的架构类型,但希望将添加类型的代码移到 XML 配置文件中。
解决方案架构
广义上说,将构建两个用户界面控件,并可以在两个类中实现:
- CreditCardValidator(信用卡验证器)
这将负责对输入的卡号进行检查,并在不正确时生成错误消息。
- CardTypesListBox(卡类型列表框)
这将向用户呈现一个下拉列表框控件,使他们能够选择他们输入的卡类型。这是一个可选组件,无需在表单上即可执行验证。
这些将依赖于一些其他类,这些类对于提供支持基础设施是必需的,即使它们能够按照之前设计中讨论的方式使用。它们是:
CreditCardValidator 基础设施类
- CreditCardValidatorDesigner
这是提供 Visual Studio .NET 设计时支持所必需的。所有验证控件都具有一个派生自 .NET Framework 的 `BaseValidatorDesigner` 类的设计器。
- CreditCardValidatorSectionHandler
此类用于使验证器能够通过 ASP.NET 应用程序的 XML 配置文件(_web.config_)进行配置。如前所述,目的是能够根据 XML 的内容生成一个卡类型集合。
CardTypesListBox 基础设施类
- CardTypesListBoxDesigner
这使得当控件放置在 ASP.NET Web 表单上时,可以为其生成设计时 HTML。
- CardTypesListBoxConverter
这是构建设计时支持的结果。`CreditCardValidator` 控件需要知道显示有效卡类型的列表框的 ID,以便进行必要的验证。与其只提供一个开发人员必须输入 `CardTypeListBox` 控件名称的字符串属性,不如它生成表单上派生自 `CardTypesListBox` 的控件的 ID 列表。
共享基础设施类
- CardType
表示单个卡类型,例如 VISA。包含在客户端和服务器端检查该类型的正则表达式,以及显示名称。
- CardTypeCollection
一个专门的 `CollectionBase` 类,存储多个 `CardType` 实例。每个卡类型都将输入到应用程序的 XML 配置文件中,并且上述 `CreditCardValidatorSectionHandler` 将创建一个包含任意数量 `CardType` 实例的此类型集合。`CreditCardValidator` 在生成客户端代码以及在服务器端检查时将访问此集合。
下面是解决方案的缩写类图,显示了组件中的关键类。大多数基础设施类出于显示原因而被排除,但将在稍后讨论。
信用卡验证器实现
总体而言,信用卡验证器控件的实现与之前版本没有太大变化,因此我不会详细讨论其实现。源代码包含足够的注释,它也不是最复杂的代码。一如既往,如果有人有任何问题,请随时给我发电子邮件,我会尽力帮助。
ASP.NET 中的所有 Web 表单验证器都派生自 `BaseValidator`。这为构建 Web 表单验证提供了一个简单但强大的模型——否则编写起来会非常繁琐。对于尚未尝试使用它的人来说,其理念是将验证器控件标签放置在页面上并分配一个要验证的控件。在回发期间,开发人员可以调用 `Page` 类型的 `IsValid` 方法来检查页面上的所有验证器是否都投票认为输入的数据正确。如果 `IsValid` 返回 true
,则页面可以处理表单数据。
要覆盖 `BaseValidator` 类中的关键方法是 `EvaluateIsValid`,这是页面调用以确定验证器负责监视的数据是否正确的方法。我们希望在此方法中实现我们的两阶段检查。首先,数字是否通过 Luhn 公式的检查——检查数字的各种位数是否正确;其次,输入的数字是否是接受的卡类型。
实现的另一个主要部分是提供 JavaScript 代码来执行与服务器端相同的检查。客户端脚本可以通过 `Page` 类型的 `RegisterClientScriptBlock` 方法输出,`CreditCardValidator` 控件将生成此代码。如上所述,这有助于减少到服务器的往返次数,这对于使用慢速连接的访问者来说非常有益(当页面使用 ViewState 时更是如此)。然而,由于用户可能关闭了 JavaScript 或正在使用不实现 JavaScript 的浏览器,因此服务器端检查也非常重要。验证控件的幸运之处在于,您可以将两者的验证集中在一个地方,提供完美的封装。
现在是时候看看验证器的各个部分是如何工作的了。
EvaluateIsValid 实现
实现的第一部分是检查 `CreditCardValidator` 类的 `CardTypesListBox` 属性是否设置了 `CardTypesListBox`。如果设置了,`FindCardTypesListBox` 方法将获取对该对象的引用。如果用户输入的卡号包含空格或其他分隔符,则通过正则表达式处理该字符串以移除除数字之外的所有内容。
如果开发人员使用了 `CardTypesListBox`,它会从选定的项目中加载正则表达式。我稍后会讨论列表框控件的实现,但它的工作方式是提供一个卡类型列表,其值设置为可用于测试号码的正则表达式。`EvaluateIsValid` 方法所需要做的就是从下拉列表中检索正则表达式。
如果开发人员没有使用 `CardTypesListBox`,它将通过调用 `ValidCardType` 方法,将卡类型与通过 XML 配置文件设置的 `CardType` 实例集合进行检查。
假设号码通过了有效卡类型检查,则通过调用该类型的 `LuhnValid` 方法,使用 Luhn 公式处理该号码。如果此检查通过,则该号码有效,并且验证器控件返回如此。下面是该方法的代码(删除了注释):
protected override bool EvaluateIsValid()
{
CardTypesListBox tl = FindCardTypesListBox ();
string cardNumber = Regex.Replace(
_creditCardNumberTextBox.Text, @"[^0-9]", ""
);
if (tl != null)
{
string regexp = tl.SelectedItem.Value;
if ( Regex.IsMatch( cardNumber, regexp ) )
return LuhnValid ( cardNumber );
else
return false;
}
else
{
if (_cardTypes != null)
{
if (ValidCardType (cardNumber))
return LuhnValid (cardNumber);
else
return false;
}
else
return LuhnValid (cardNumber);
}
}
现在最好看看 Luhn 公式检查卡类型的实现。然后我将讨论卡类型正则表达式检查,然后谈论 `CardTypesListBox` 控件。
Luhn 公式实现
首先,了解一下基本的卡号验证算法是个不错的选择,然后我将讨论实际实现此检查的 C# 代码。
Luhn 公式
该公式在网上有很多详细的文档,所以这只是作为算法的一个提醒或概述。大致有 4 个步骤:
- 交替数字的值加倍
第一步是将每隔一个数字加倍,从倒数第二个数字开始。例如,如果我们取数字 1234-5678-1234-5670,我们将这样做:
7 * 2 = 14 5 * 2 = 10 3 * 2 = 6
并以此类推直到结束。
- 将上一阶段产品中的各个数字相加
(1 + 4) + (1 + 0) + (6) + ...
对于第一阶段提到的数字,这使我们总共得到 28。
- 添加未受影响的数字
这次我们添加在步骤 1 中没有加倍的数字的位数。同样,从右边开始,它看起来像这样:
0 + 6 + 4 + 2 + 8 + 6 + 4 + 2 = 32
- 将两个结果相加并除以 10
如果两个结果相加后除以 10 没有余数,则为有效号码。
(Result from Stage 2 + Result from Stage 3) mod 10 = 0 = valid 28 + 32 = 60 mod 10 = 0
C# 实现
原始验证器控件包含一个相当冗长的实现,并没有真正优化。我得到了 Frode N. Rosland 提供的以下实现:
private static bool ValidateCardNumber(string cardNumber)
{
int length = cardNumber.Length;
if (length < 13)
return false;
int sum = 0;
int offset = length % 2;
byte[] digits = new System.Text.ASCIIEncoding().GetBytes(cardNumber);
for (int i = 0; i < length; i++)
{
digits[i] -= 48;
if (((i + offset) % 2) == 0)
digits[i] *= 2;
sum += (digits[i] > 9) ? digits[i] - 9 : digits[i];
}
return ((sum % 10) == 0);
}
它通过首先将每个数字从字符串转换为字节来工作,将数字减去 48,以便每个数字都可以作为数字进行处理。作为字节处理避免了以前所有的类型转换开销,并且优化了最终求和的添加。
一旦它有了一个要处理的数字数组,它就会遍历该长度。如果它是一个偶数位,那么它会将值加倍,然后加到总和中。它不是执行阶段 2,然后阶段 3,最后将两个值相加,而是两个阶段都加到同一个总和中。
求和操作通过判断数字是否被加倍来工作。如果被加倍且大于 9,我们可以通过从字节值中减去 9 来计算要添加的值。例如,假设卡号中的原始数字是 7,然后加倍为 14。14 在十六进制中表示为 E,如果我们将 9 从 14 中减去 (E - 9),我们剩下 5。如果我们通过将两个数字相加 (1 和 4) 来计算,我们也会得到 5。如果数字不大于 9,则无需进行任何额外计算,只需将数字加到总和中即可。
最终检查是通过返回结果总和模 10 与 0 之间的布尔比较来完成的——即总和是否能被 10 整除,没有余数。
正则表达式卡类型检查
在 `CreditCardValidator` 的 `OnInit` 事件期间,卡类型从应用程序的 XML 配置文件加载,为每种卡类型生成 `CardType` 实例的集合。这些实例由 `CardTypeCollection` 类存储(一个简单的强类型集合,派生自 `CollectionBase`)。
`CardType` 类型有两个属性:`Name` 和 `RegExp`。`Name` 用于提供卡类型的人类可读名称,例如 MasterCard 或 VISA。`RegExp` 属性存储用于检查卡类型是否为正确格式的正则表达式(字符串形式)。
正则表达式
对于不熟悉正则表达式的人来说,它们提供了一种处理文本的方法,例如执行模式匹配、提取或替换数据以及许多其他事情。在 Google 上搜索正则表达式应该会提供大量的材料供查阅。然而,现在快速概述它们与卡类型检查的关系也无妨。
可以生成模式匹配正则表达式来检查卡号与卡类型的有效性。例如,以下正则表达式检查 VISA 信用卡号的有效格式:
^[4] ( [0-9]{15}$ | [0-9]{12}$ )
第一个语句(粗体)检查数字是否以数字 4 开头。所有 VISA 卡号都以 4 开头。正则表达式的下一部分由括号分组,然后两个语句由竖线符号分隔(这等同于 OR 语句)。第一个语句(斜体)检查最后 15 个字符($ 符号将搜索锚定到字符串的末尾)是否都是数字。总的来说,前两个命令检查以 4 开头的 16 位数字。OR 命令后的第二个语句检查 0 到 9 之间的 12 位数字,允许包括 4 前缀在内的 13 位数字。
如前所述,每种卡类型都从 XML 配置文件中加载,这是通过调用 `CreditCardValidator` 类型的 `LoadCardTypes` 方法加载的,其工作方式如下:
CardTypeCollection ct =
(CardTypeCollection)ConfigurationSettings.GetConfig(
"Etier.CreditCard"
);
这会加载一个 `CardTypeCollection`,该集合是通过检索 `Etier.CreditCard` 部分构建的,其内容如下(请注意 `configSection` 元素分行显示,这纯粹是为了显示原因):
<configSections>
<section
name="Etier.CreditCard"
type="Etier.CreditCard.CreditCardValidatorSectionHandler,
CreditCardValidator, Version=0.0.0.1" />
</configSections>
<Etier.CreditCard>
<cardTypes>
<cardType name="VISA" regExp="^[4]([0-9]{15}$|[0-9]{12}$)" />
<cardType name="MasterCard" regExp="^[5][1-5][0-9]{14}$" />
<cardType name="American Express" regExp="^[34|37][0-9]{14}$" />
etc...
</cardTypes>
</Etier.CreditCard>
第一个语句创建一个新的 `configSection`——允许开发人员处理他们自己的自定义应用程序设置。如上所示,该部分被赋予一个名称,实现此部分处理程序的类型、其版本和程序集名称。
CreditCardValidatorSectionHandler 实现
要创建配置节处理程序,类型必须提供 `IConfigurationSectionHandler` 接口的实现。该接口指定了一个方法——`Create`。最后一个参数是最重要的,它返回我们从 XML 配置文件加载的节的 `XmlNode` 实例,允许我们解析设置。
当从 `CreditCardValidator` 类型调用 `GetConfig` 时,它会传递节名称——这使我们能够获取 `Etier.CreditCard` 节点的编程表示。这反过来又允许我们解析 `cardTypes` 元素和每个 `cardType` 子元素,从而为我们提供完整的卡类型集合。
下面是 `CreditCardValidatorSectionHandler` 类型的 `Create` 方法的实现:
XmlNodeList cardTypes;
CardTypeCollection al = new CardTypeCollection();
cardTypes = section.SelectNodes("cardTypes//cardType");
foreach( XmlNode cardType in cardTypes )
{
String name = cardType.Attributes.GetNamedItem("name").Value;
String regExp = cardType.Attributes.GetNamedItem("regExp").Value;
// Add them to the card types collection
CardType ct = new CardType(name,regExp);
al.Add(ct);
}
return al;
对配置节(" `Etier.CreditCard` ")执行 XPath 查询,这使我们能够为每个 `cardType` 元素检索 `XmlNode` 实例列表。然后遍历这些实例,检索 `name` 和 `regExp` 属性,并用于创建新的 `CardType` 实例。每个 `CardType` 实例随后可以存储在 `CardTypeCollection` 中,然后 `CreditCardValidator` 在其 `OnInit` 事件期间检索该集合(如前所述)。
Client Side Validation
添加 JavaScript 代码来执行与服务器控件相同的检查是我在编写第一个版本后想要添加的关键功能之一。
如本文开头所述,我收到了几封来自人们的电子邮件,提到他们已为原始 CodeProject 文章添加了客户端脚本支持。我保留了大部分贡献的代码不变,但我更改了卡类型检查代码,使其使用与服务器端检查相同的正则表达式——允许通过在 XML 配置文件中添加一行来在客户端和服务器端添加新的卡类型。
客户端脚本在调用该类型的 `RegisterClientScript` 方法时发出。此方法首先生成代码,然后通过调用其 `RegisterClientScriptBlock` 方法将其添加到父 `Page` 对象。
下面是生成 JavaScript 验证代码的 `RegisterClientScript` 方法的片段:
protected void RegisterClientScript()
{
this.Attributes["evaluationfunction"] = "ccnumber_verify";
StringBuilder sb_Script = new StringBuilder();
sb_Script.Append( "<script language=\"javascript\">" );
sb_Script.Append( "\r" );
sb_Script.Append( "\r" );
sb_Script.Append( "function ccnumber_verify() {" );
sb_Script.Append( "\r" );
sb_Script.Append( "var strNum = document.all[document.all[\"" );
sb_Script.Append( this.ID );
sb_Script.Append( "\"].controltovalidate].value;" );
sb_Script.Append( "\r" );
...
Page.RegisterClientScriptBlock ("CreditCardValidation",
sb_Script.ToString());
该方法的第一行设置 `CreditCardValidator` 控件的一个属性,提供了服务器端 `EvaluateIsValid` 方法的客户端等效项。`ccnumber_verify` 函数实现了与服务器端相同的功能。
有两个客户端函数 `isNumberValid` 和 `isCardTypeCorrect`。`isNumberValid` 执行 Luhn 检查,其代码如下:
function isNumberValid(strNum) {
var nCheck = 0;
var nDigit = 0;
var bEven = false;
for (n = strNum.length - 1; n >= 0; n--) {
var cDigit = strNum.charAt (n);
if (isDigit (cDigit)) {
var nDigit = parseInt(cDigit, 10);
if (bEven) {
if ((nDigit *= 2) > 9) nDigit -= 9;
}
nCheck += nDigit;
bEven = ! bEven;
}
else if (cDigit != ' ' && cDigit != '.' && cDigit != '-')
return false;
}
return (nCheck % 10) == 0;
}
`isCardTypeCorrect` 函数的实现取决于是否使用了卡类型列表框。如果存在列表框,则(与服务器端一样)将指定要检查的卡类型。如果没有使用列表框,则应检查卡号是否与所有卡类型匹配。
以下代码显示了当 `CardTypesListBox` 属性已设置时如何生成 JavaScript:
StringBuilder sCardTypeFunction = new StringBuilder();
sCardTypeFunction.Append( "function isCardTypeCorrect(strNum) {");
sCardTypeFunction.Append( "\r" );
// If the listbox is being shown, accept only the selected one.
if (_cardTypesListBox != null)
{
sCardTypeFunction.Append( " return strNum.match(" );
sCardTypeFunction.Append( "document.all[\"" +
_cardTypesListBox + "\"].value" );
sCardTypeFunction.Append( ")");
sCardTypeFunction.Append( "\r" );
sCardTypeFunction.Append( "}" );
sb_Script.Append( sCardTypeFunction.ToString() );
}
由于卡类型列表框包含用于检查有效卡类型的正则表达式,因此可以检索并使用 match JavaScript 函数执行它。
如果开发人员没有使用下拉列表,则执行以下代码生成客户端代码:
sCardTypeFunction.Append( " if ( " );
foreach( CardType ct in _cardTypes)
{
sCardTypeFunction.AppendFormat("strNum.match(\"{0}\")", ct.RegExp);
i++;
if (i < _cardTypes.Count)
sCardTypeFunction.Append(" || ");
}
sCardTypeFunction.Append(" ) \r");
sCardTypeFunction.Append(" return true;");
sCardTypeFunction.Append( "\r" );
sCardTypeFunction.Append(" else" );
sCardTypeFunction.Append( "\r" );
sCardTypeFunction.Append(" return false;");
sCardTypeFunction.Append( "\r" );
sCardTypeFunction.Append( "}" );
卡类型被迭代以生成一个大型条件 OR 语句,检查卡类型集合中的所有正则表达式。只要其中一个成功,函数就会返回 true
,表示卡类型有效。
如果不存在卡类型,则 `isValid` 卡类型检查将被忽略,仅使用 Luhn 公式。
最后一个功能是添加了 Visual Studio .NET 设计时支持,允许开发人员将验证控件拖放到表单上。
设计时支持
我希望为该控件提供 Visual Studio .NET 支持,以便为使用 IDE 的开发人员提供更集成的组件。下面是它在设计视图中的 Web 表单上的显示截图,以及添加到 IDE 工具箱中的控件。
当一个控件从工具箱拖到编辑器时,设计器会加载程序集并识别用于生成表示该控件的 HTML 的关联设计器类型。可以覆盖设计器的基本实现并提供自定义设计时 HTML。
由于验证控件没有做很多事情,所以自定义设计器并不是特别必要,但是在使用 `BaseValidatorDesigner` 直接设计时,它确实在 IDE 中引起了一些问题,因此转而派生了一个类型。这也是为了支持自定义属性类型。
要将控件与设计器关联,请使用 `DesignerAttribute` 属性,如下所示:
[
Designer(typeof(CreditCardValidatorDesigner))
]
public class CreditCardValidator : BaseValidator
尽管这提供了实现拖放放置所需的一切,但 `CreditCardValidator` 除了标准设计时支持之外,还添加了一些额外的属性。
例如,为了使信用卡验证器能够与卡类型下拉菜单一起工作,需要告知验证器列表框的 ID,以便在运行时可以引用它。尽管这可以只是一个文本字段,要求用户手动输入名称,但如果在设计器中显示相关的下拉框会更好。下面是 Visual Studio .NET IDE 中自定义设计器使用的图片:
这是这样实现的:
[
TypeConverter (typeof(CardTypesListBoxConverter))
]
public string CardTypesListBox
{
`TypeConverterAttribute` 属性用于告诉 Visual Studio .NET 设计器它可以用来向开发人员显示选项的类型转换器。由于验证器控件的 `CardTypesListBox` 属性是一个字符串,因此 `CardTypesListBoxConverter` 派生自 `StringConverter`。
`CardTypesListBoxConverter` 类型的关键方法是 `GetStandardValues`,它旨在返回该属性可接受值的集合。在 `CardTypesListBoxConverter` 类型中,它被实现为:
public override StandardValuesCollection GetStandardValues(
ITypeDescriptorContext context
)
{
if (context == null || context.Container == null)
{
return null;
}
object[] locals = GetControls(context.Container);
if (locals != null)
{
return new StandardValuesCollection(locals);
}
else
{
return null;
}
}
`Container` 属性用于允许类型转换器更多地了解它所处的环境(Web 表单)。以上代码只是调用了 `GetControls` 方法,该方法检索表单上 `CardTypesListBox` 控件名称的 `ArrayList`。通常应该只有一个,但通过使用以下代码,可以确保开发人员不会拼错。`GetControls` 方法的实现如下:它遍历页面上的组件集合,当遇到 `CardTypesListBox` 类型的组件时,将其添加到 `ArrayList` 中,然后对 `ArrayList` 进行排序并返回。
private object[] GetControls(IContainer container)
{
ComponentCollection componentCollection = container.Components;
ArrayList ctrlList = new ArrayList();
foreach( IComponent comp in componentCollection )
{
Control ctrl = (Control)comp;
if (ctrl is CardTypesListBox)
{
if (ctrl.ID != null && ctrl.ID.Length != 0)
{
ValidationPropertyAttribute vpa =
(ValidationPropertyAttribute)TypeDescriptor.GetAttributes(ctrl)
[typeof(ValidationPropertyAttribute)];
if (vpa != null && vpa.Name != null)
ctrlList.Add(String.Copy(ctrl.ID));
}
}
}
ctrlList.Sort();
return ctrlList.ToArray();
}
信用卡验证器摘要
总而言之,信用卡验证器的首要任务是从 XML 配置文件加载卡类型。这些卡类型可以表示为正则表达式,并从 Web 应用程序的 _web.config_ 加载。通过将它们存储为正则表达式,它们既可以用于服务器端验证,也可以用于客户端验证,同时还允许添加新的卡类型而无需构建和部署新的程序集。
任何派生自 `BaseValidator` 的验证器的关键方法是 `EvaluateIsValid`,对于信用卡验证器,它首先检查是否输入了有效的卡类型(如果使用下拉列表,则对照用户选择的卡类型进行检查——此控件将在下面讨论),如果是,则使用 Luhn 公式检查号码。如果两项检查都通过,则假定为有效号码,并且控件投票表示验证成功。
假设 Web 表单上的其他验证器控件也验证成功,则 `Page.IsValid()` 调用将返回 true
,表示表单包含有效数据。
从客户端的角度来看,代码注册了一个由页面呈现的脚本块,该脚本块又实现了等效的 JavaScript 代码,使用 Luhn 公式和自定义正则表达式检查号码类型。
设计时支持在很大程度上是自动处理的,除了添加了一个类型转换器,使控件能够列出要显示接受的卡类型的列表框控件。`CardTypesListBox` 控件是接下来要查看的内容。
CardTypesListBox 实现
如讨论解决方案架构时所述,此控件的目的是补充验证器,使访问者能够从下拉列表中选择卡号。与验证控件相比,其实现相对简单。
由于本文已经相当长,我将不再讨论列表框大部分的直接实现。
由于应用程序配置文件包含卡类型列表,因此列表框相对独立,并且使用与信用卡验证器相同的命令从文件中加载详细信息。与验证器控件一样,此操作在 `OnInit` 事件期间执行。
然而,为了使控件能够放置到设计器中,而设计器中尚无应用程序配置设置可用,有必要确定我们是在运行时环境还是设计时环境。
protected override void OnInit(System.EventArgs e)
{
if (Site != null)
{
if (Site.DesignMode)
// In design mode so just add a quick dummy item.
Items.Add( new ListItem("CardTypes Here") );
}
else
{
CardTypeCollection ct = (CardTypeCollection)
ConfigurationSettings.GetConfig( "Etier.CreditCard" );
正如您所看到的,它与信用卡验证器 `OnInit` 事件期间使用的代码极其相似。
为卡类型列表框添加支持的大部分工作都在验证器控件中。由于验证使用正则表达式,因此可以将正则表达式加载到列表框中,然后直接从那里检索。实际使用的是访问者选择的卡类型的正则表达式。
如何使用该控件
顶部的链接包含一个演示项目,其中包含如何使用该控件的示例。但是,这里是所需的步骤。
- 将 `CreditCardValidator` 程序集复制到应用程序的 _/bin_ 目录中。
- 根据需要编辑 _web.config_ 配置文件。虽然不必列出卡类型,但建议这样做,因为它提供了额外的检查级别。
- 在页面顶部添加 ASP.NET 控件标签前缀声明,如下所示:
<%@ Register TagPrefix="etier" Namespace="Etier.CreditCard" Assembly="CreditCardValidator" %>
- 按如下方式声明验证控件(必要时更改 `ControlToValidate` 和其他属性):
<etier:CreditCardValidator Id="MyValidator" ControlToValidate="CardNumber" ErrorMessage="Please enter a valid credit card number" RunAt="server" EnableClientScript="True"/>
- 如果使用卡类型列表框,请按如下方式将列表框控件添加到页面:
<etier:CardTypesListBox id="CardTypesListBox1" runat="server" Width="198px" Rows="1">
并设置验证器控件的 `CardTypesListBox` 属性
CardTypesListBox="CardTypesListBox1"
- 在表单回发期间处理数据时,请确保调用 `IsValid()`,并且仅当它返回
true
时才处理表单数据。
要从 Visual Studio .NET 中使用该控件,您需要右键单击工具箱,选择“添加/删除项...”,然后浏览已编译的程序集。然后仔细检查 `CreditCardValidator` 和 `CardTypesListBox` 组件是否已列出并在列表框中选中。然后您可以将它们拖放到页面上。如果您想使用卡类型列表框,还需要在应用程序的 XML 配置文件中添加卡类型。
结论
这篇文章写得相当长,比我 2002 年最初写的时候扩展了很多。我非常感谢那些使用过我的代码并觉得它有用的人,也感谢大家发邮件给我提出的所有建议。