Wednesday, October 20, 2004

 

How to deploy a .Net assmebly for COM use through CAB on webpage

1.
通过CAB包和IE在客户端安装可被COM调用的.Net assembly
By Wang Ting
From http://blog.joycode.com/felix/archive/2004/09/08/32909.aspx

普通的ActiveX控件或COM DLL打包成Cab后能很容易通过Internet Component Download装到客户端。我们只需在网页里加入 object codebase... /object>。

今天遇到一个人想对.Net写的“COM组件”做相同的事情。可是.Net assembly是要RegAsm之后才能被COM client调用的……

一个解决办法是把.Net assembly和它的TLB(Register属性都为vsdrfCOM)打在Setup project中生成一个MSI,然后再把MSI打在CAB中。我们可以通过CAB INF中的Hook功能,在客户端下载完CAB之后自动安装CAB中的MSI。

详细步骤

To create a .Net assembly which will be used by COM clients:

1. Create a VB.Net Class Library named “ClassLibraryVBActiveX”. Use the following code for the project:

Imports System.Runtime.InteropServices
_
Public Class Class1
Public Function Hello() As String
Return "Hello World!"
End Function
End Class

Please keep a note of the GUID. We will use it later.

2. Right click the class library project in the Solution Explorer and select “Properties”. This will open the Property Pages of the class library project. Under the category “Configuration Properties” | “Build”, check the check box “Register for COM Interop”.

3. Build the class library project.

To create a Setup project for the class library:

4. Right-click the solution in the Solution Explorer and select “Add” | “New Project”. Select “Setup Project” and name it as “ActiveXInstaller”.

5. Add the “Primary output” of the class library to the Setup project.

6. Build the Setup project. We should see that “ClassLibraryVBActiveX.dll” and “ClassLibraryVBActiveX.tlb” have been built into the Setup project.

To build the CAB file for the Setup project:

7. Extract the ZIP attached and save the files to a temporary folder.



We have the following 5 files:

makecab.exe – The utility for building CABs. The utility can typically be found in “System32”.

Installer.inf – The Inf file for the CAB file. It has the following contents:

[Setup Hooks]
hook1=hook1

[hook1]
run= msiexec /i %EXTRACT_DIR%\ActiveXInstaller.msi /qn

[Version]
; This section is required for compatibility on both Windows 95 and Windows NT.
Signature="$CHICAGO$"
AdvancedInf=2.0

Here “run” specifies the command we would like to run after IE extracts the CAB. The command line will install the MSI “ActiveXInstaller.msi” under silent mode. You may refer to the following links for more details about the format of INF:

About INF File Architecture
http://msdn.microsoft.com/library/default.asp?url=/workshop/delivery/download/overview/infarchitecture.asp?frame=true

Cab.DDF – a text file with the following contents:

.OPTION EXPLICIT
.Set Cabinet=on
.Set Compress=on
.Set MaxDiskSize=CDROM
.Set ReservePerCabinetSize=6144
.Set DiskDirectoryTemplate="."
.Set CompressionType=MSZIP
.Set CompressionLevel=7
.Set CompressionMemory=21
.Set CabinetNameTemplate="ActiveX.CAB"
"Installer.inf"
"ActiveXInstaller.msi"

This file is a directive file for the “MakeCAB.exe” utility. “ActiveX.CAB” is the output name of the CAB. The last two lines indicate the two files we would like to include into the CAB.

Make.bat – a batch file that will run the following command to build the CAB:

MAKECAB.EXE /f "Cab.DDF"

TestPage.htm – a template HTML that will install the CAB:

HTML>
BODY>
OBJECT ID="ClassLibraryVBActiveX"
CLASSID="CLSID:BC2E2273-E3D8-4AEA-8A4F-799574803D89"
CODEBASE="ActiveX.CAB#version=1,0,0,0">
/OBJECT>
/BODY>
/HTML>

Note that the GUID should match the GUID in our VB.Net code.

8. To use these files, please copy output MSI of the Setup project (e.g. “ActiveXInstaller.msi”) to the same folder as the files above. Then double-click “Make.bat” to build the CAB.

9. Copy the CAB as well as “TestPage.htm” to a web folder and test it.

Note that this approach also provides a way for the end user to uninstall the assembly. We can remove it by “Add/Remove Programs” applet.

2.
http://www.sellsbrothers.com/wahoo/

re: 9/11/2004 12:13 PM Wang Ting
其实差别还是很大的……
这个网站是用一个MSI来修改.Net CLR Security Policy,以便可以以full trust来运行它的.Net程序(一样要下载到本地,我个人觉得还不如直接让用户下一个EXE或script运行)我这里做的是通过CAB包把.Net assembly安装注册成COM组件,以便COM client可以调用……

3.
9/15/2004补充
居然没有看见Junfeng Zhang的这一篇,惭愧

You can deploy assembly using cab files.

http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpguide/html/cpcondeployingcommonlanguageruntimeapplicationusingcabfiles.asp

There are two ways to load assemblies from cab files.

1. Assembly.LoadFrom

You can give the URL of the cab file to Assembly.LoadFrom. For example,

Assembly.LoadFrom(“http://myserver/myasm.cab”);

2. Use app.config, and specify codebase hint, then use Assembly.Load.

For example:













When we locate the cab file, we extract them to a temporary directory. The temporary directory becomes the application base now and we will probe the desired assembly in the temporary directory. We then copy the assembly to download cache, and remove the temporary directory we created. The final assembly location you see from Assembly.Location will be in download cache directory.

There are subtle difference in using Assembly.LoadFrom and codebase hint.

In Assembly.LoadFrom case, after we extract the cab file, we will only look for assembly with the same name as the cab file, excluding the extension. In our example above, we will only look for myasm.dll/myasm.exe.

In codebase hint case, we will look for the assembly specified in Assembly.Load, instead of the name of the cab file. This means, the cab file does not have to have the same name as the assembly.

A side effect of codebase hint behavior is that you can put multiple assemblies in one cab file. You will need to specify multiple assemblyBinding statements in app.config file to use the same cab file as codebase hint for different assemblies.

If you use this technique, due to how we extract the cab file and find the assembly (see above), we will download, and extract the same cab file multiple times. So bundling multiple assemblies in one cab file may actually hurt performance, instead of improving performance.

Of course, in reality, we use IE to download the cab file. You may benefit from IE’s caching of the downloaded cab file. You really want to measure your scenario, to decide if you want to use this technique or not.

Tuesday, October 19, 2004

 

Spoof the Referrer

1.
plog中添加资源防盗链

By bruce
From http://blog.9zi.com/post/1/928

网站流量占用最大的是文件下载、大尺寸图片。很多人喜欢引用其他站点的图片,导致非本站显示访问的流量猛增加。

不管出于什么原因,各种防盗链手段都来了。


防盗链原理:

http标准协议中有专门的字段记录referer

一来可以追溯上一个入站地址是什么

二来对于资源文件,可以跟踪到包含显示他的网页地址是什么。

因此所有防盗链方法都是基于这个Referer字段

网上比较多的2种

一种是使用apache文件FileMatch限制,在httpd.conf中增加
SetEnvIfNoCase Referer "^http://host1\.vhost\.com/" local_ref=1
SetEnvIfNoCase Referer "^http://host2\.vhost/" local_ref=1 #虚拟主机地址访问
SetEnvIfNoCase Referer "^http://202\.112\.20\.108/" local_ref=1 #ip访问
FilesMatch "\.(ibfipbgifpngjpgjpeg)"> #大小写忽略
Order Allow,Deny
Allow from env=local_ref
/FilesMatch>
这种很方便禁止非允许访问URL引用各种资源文件

第二种是使用rewrite,需要增加apache的mode_rewrite,支持.htaccess文件目录权限限制
在虚拟主机根目录增加.htaccess文件,描述从定向,把非本地地址refer的图片文件都从定向到警告图片上。
RewriteEngine On
RewriteCond %{HTTP_REFERER} !^$
RewriteCond %{HTTP_REFERER} !^http://(www\.)?myhost.net /.*$ [NC]
RewriteRule \.(pngjpeggifjpg)$ http://myhost.net/abc.gif [R,L]
有个好处是,不同的虚拟主机用不同的描述定义。

我在解决plog禁止盗链的时候,发现个问题,也算个好方法。
plog把所有资源都自己管理起来,用resserver.php来动态显示,这样统一的入口方便添加权限操作。
同时造成上面2种方法无法使用,因为不再是apache直接访问资源文件,而是php通过文件读取。

因此只能在代码中做手脚:在读取资源文件输出之前,加如下判断代码

$referer = $_SERVER['HTTP_REFERER'];
$selfurl = $_SERVER['HTTP_HOST'];
if(false == strpos($referer,$selfurl))
{
echo '非法盗链!';
exit(1);
}
这里有些偷懒,直接看引用地址中是否包含host地址,不过原理就是这样,判断referer是否是本站地址。

我们常常在下载的时候,也碰到盗链网站无法下载,报盗链的问题。要下载这类文件最简单的方法就是改referer

比方flashget中,网址下面的"引用"一栏中,直接填写下载地址就可以了。

2.
网站图像防盗Apache配置有妙法

来源不详

每个网站所有者都在尽力美化自己的网站,使它看上去更酷、更具有吸引力,其中最常见的方法就是使用图片、Logo及Flash等。但是,这也会带来一个问题,因为越漂亮、越吸引人的网站,漂亮的图片和Flash等就容易被别的网站悄悄的盗用。下面我们就一起讨论如何防止网站图像被盗用。

需要解决的问题

简单的说,这里有两种不同的盗用行为:
1. 使用HTML标记IMG在自己的网站中引用网站的图片。
2. 从网站上下载图片,然后放在自己的网站上。

对于第一种的盗用行为,合法网站的图片被用来美化装饰其它网站,这种盗用对合法网站的损害比较大,因为访问非法网站的访问者其实是从合法网站获取图片的,合法网站的日志文件充满了访问请求记录,并且带宽被非法访问消耗,而合法网站却没有得到任何好处。这种类型的盗用通过技术手段完全可以被防止。

第二种类型的盗用相对来说比较阴险,浏览者在非法网站直接访问非法的图片,而合法网站的版权受到侵害,却得不到赔偿,甚至无法发现这种盗用。因为Web的工作方式对这种类型的盗用实际上无法被阻止,但是可以使得这种盗用更加困难。

完全杜绝这两种盗用行为是不现实的,但是通过技术手段可以使得这种盗用非常困难。在Apache环境下,通过配置可以限制网站图片被盗用。

标识需要保护的文件

作为网站管理员,最大的希望就是能够保护网站上所有文档,但是从技术角度考虑这种想法是不现实的,因此我们这里只讨论对图片文件的保护。

作为保护的第一步,首先需要标识出需要保护的文件,然后才能进一步对被标识的文件进行保护。在Apache配置文件中添加如下内容:



[这里添加保护限制命令]



将容器命令包含在或等容器中,或者单独列出,不处于任何保护容器中,这样就会对网站所有文件进行保护,甚至可以存放在.htaccess文件。将该容器放在不同的位置,保护的范围机会有所不同。

Referer HTTP头字段

当用户访问Web服务器请求一个页面时,用户浏览器发送的HTTP请求中会有一个被称为HTTP请求头(HTTP Request Header)的信息,这个头信息中包含客户请求的一些信息,例如发出请求客户主机的浏览器版本、用户语言、用户操作系统平台、用户请求的文档名等,这些信息以变量名/变量值的方式被传输。

在这些信息中,Referer字段对于实现防止图片盗用非常重要。Referer字段指定客户端最后一个页面的URL地址。例如,如果用户访问页面A,然后点击在页面A上到页面B的链接,访问页面B的HTTP请求会包括一个Referer字段,该字段会包括这样的信息“这个请求是来自于页面A”。如果一个请求不是来自于某个页面,而是用户通过直接在浏览器地址栏输入页面A的URL地址的方式来访问页面A,那么在HTTP请求中则不会包括Referer字段。这样对于我们防止盗链有什么帮助呢?Referer字段是帮助判断对图像的请求是来自自己的页面,还是来自其它网站。

使用SetEnvIf对图像进行标记

作为一个简单的例子,假设需要保护的网站的主页面为http://my.apache.org,这时候希望限制所有不是源于本网站的网络访问请求(例如只允许访问包含在本网站页面内的图片)。这里可以使用一个环境变量作为一个标记,如果条件满足时就设置该变量,如下所示:
SetEnvIfNoCase Referer "^http://my\.apache\.org/" local_ref=1

当Apache处理一个请求时,它会检查HTTP请求头中的Referer字段,如果该请求来源于本网站(也就是请求页面的URL为本网站域名),则设置环境变量local_ref为1。

在双引号中的字符串是一个正则表达式,只有匹配该正则表达式,环境变量才会被设置。本文不讨论如何使用正则表达式,这里只需要理解SetEnvIf*命令会使用正则表达式作为参数。

SetEnvIfNoCase命令的“NoCase”部分表示这里的正则表达式忽略大小写,'http://my.apache.org/'、'http://My.Apache.Org/'或 'http://MY.APACHE.ORG/'都可以匹配条件。

在访问控制中使用环境变量

Apache配置文件中的Order、Allow和Deny命令可以实现对文档的基于环境变量的访问控制,使用Order、Allow和Deny命令首先要考虑的是Allow和Deny命令的顺序对于Apache处理结果的影响,应该以下面的方式使用:
Order Allow,Deny

这里表示Apache首先处理该HTTP请求相关的Allow命令,然后处理相关的Deny命令。这种处理方式的默认策略是Deny,所以除非有明确的允许的设置,否则该请求就会被拒绝,任何非法访问将无法成功。

因此,在Apache的配置文件httpd.conf中添加如下命令,来实现本地引用发挥作用:

Order Allow,Deny
Allow from env=local_ref

这样只有在local_ref变量被定义的情况下,该请求才会被允许;否则其它所有请求和访问将会被拒绝,因为这些请求不满足Allow条件。

注意,请不要在.htaccess和httpd.conf中使用容器命令,这里不需要该容器命令,除非有特殊的需求,例如希望Get请求和Post请求进行不同的处理。

把这些相关设置放在一起,在Apache的配置文件中就会有如下内容:

SetEnvIfNoCase Referer "^http://my\.apache\.org/" local_ref=1
FilesMatch "\.(gifjpg)">
Order Allow,Deny
Allow from env=local_ref
/FilesMatch>

如上配置可以存放在服务器配置文件httpd.conf中,或者存放在.htaccess文件中,最后的效果是一样的:在这些命令作用的范围内,只有从本网站引用的图片才可以被访问。

对图片进行水印处理

上面介绍的方法并不能完全防止图像盗链,这是因为有些执著的盗用者可以伪造Referer值来盗用图片,使相关设置失效,所以不可能完全防止网站图片被盗链,但是上面采取的措施会使得盗链变得很困难。

此外,还有一个防止图片被盗用的方法,就是对网站的图片都进行水印处理。对一个数字图片进行水印处理是指在图片中加入一个特殊的签名编码,并且可以进行验证和检测,数字水印并不会降低图片的质量,甚至可以实现图像被切割以后的剩余部分仍然会包括水印信息。图片被再次编辑、打印,并再次扫描以后,水印仍然可以被检测到。因此,水印技术是一个非常好的保护图片不被盗用的技术。

记录盗用请求

如果想知道自己网站的艺术品是否被盗,可以尝试使用同样的侦测和环境变量来记录可疑请求。例如,在httpd.conf文件中添加如下命令,那么会在/usr/local/web/apache/logs/poachers_log文件中记录所有具有非法的Referer头信息的访问请求:


SetEnvIfNoCase Referer "!^http://my\.apache\.org/" not_local_ref=1
SetEnvIfNoCase Request_URI "\.(gifjpg)" is_image=1
RewriteEngine On
RewriteCond ${ENV:not_local_ref} =1
RewriteCond ${ENV:is_image} =1
RewriteRule .* - [Last,Env=poach_attempt:1]
CustomLog logs/poachers_log CLF env=poach_attempt

在上面代码中,头两行为条件设置标记(也就是没有正确的本地Referer的图片文件),RewriteCond检测是否该标记被设置,然后RewriteRule设置第三个标记,最后一行使得这样的访问请求被记录在特定的文件中。

上面简单介绍了在Apache环境下,如何通过配置来限制网站图片被盗用的方法,抛砖引玉,希望大家将自己更好的经验介绍出来。

3.
Spoofing the Referrer using HttpWebRequest
By Dave Wanta on VB.NET
From http://www.developerfusion.co.uk/show/4672/

noticed the article the other day on your website about "Spoofing the Referer During a Web Request" Immediately after reading it I was wondering if you can do this using ASP.NET, the answer is a resounding "YES, of course!". This works because the http standards allow the client to actually dictate the HTTP_Referer variable.

Here is the code:


Function FetchURL(SomeURL as String, Referer As String) as String
Dim WebResp as HTTPWebResponse
Dim HTTPGetRequest as HttpWebRequest
Dim sr As StreamReader
dim myString as String
HTTPGetRequest = DirectCast(WebRequest.Create(SomeURL),HttpWebRequest)
HTTPGetRequest.KeepAlive = false
HTTPGetRequest.Referer = Referer
WebResp = HTTPGetRequest.GetResponse()
sr = new StreamReader(WebResp.GetResponseStream(), Encoding.ASCII)
myString = sr.ReadToEnd()
Return myString
End Function
You can then call this using the following:

Dim PageString As String
PageString = FetchURL("http://www.google.com/","http://www.microsoft.com")

4.
Proposal on referrer spam: Background and blacklists

From http://underscorebleach.net/jotsheet/2005/01/referrer-spam-proposal

Referrer (or referer) spam has become a serious problem in the blogosphere. We need an intelligent way to eliminate this growing nuisance. I've thought about and researched this for the past few days, and below I offer a proposal for a technological solution to this problem. It requires programming, and I am not a programmer, so I welcome suggestions, corrections, and improvements to this proposal.
I hope that this blog entry can serve as something of a starting point for information about referrer spam as well as a sandbox for exchanging ideas about methods of curbing or eliminating it.
Table of Contents
Background
Doesn't rel="nofollow" solve the problem?
Recommended webmaster practices
The .htaccess arms race is unwinnable
Technical characteristics of referral spam
Idea #1: Filter referrer URLs against Jay Allen's MT-Blacklist
Idea #2: Filter referrer IPs against spam blacklists
Conclusion
Addenda
Other resources
TrackBacks
Comments
Post a comment
Background
I will not go into much detail about the definition or origins of or reasons for referral spam. Please refer to other sites for that. I will mention that spammers are not stupid, and their activities always have a purpose. Spammers' activites consume their own resources, and as long as bloggers continue to publish records of referrers, it will be profitable and worthwhile for referral spammers to continue in their endeavors.
Doesn't rel="nofollow" solve the problem?
As you may have heard (or were hinted at), an illustrious coalition of blogging and search engine companies recently announced support for a new HTML attribute designed primarily to combat comment spam. Potentially, it's even more effective for referral spam. The attribute is called rel="nofollow",and many bloggers are already praising it as the silver bullet the Web's been waiting for.
The idea is actually quite simple; the hard part was getting the major players (Google, Yahoo, MSN, etc.) to agree on it. Basically, if a link is tagged with the rel="nofollow" attribute, it won't contribute to that site's PageRank. ("PageRank" is a Google-specific term, but I'm using it in the generic sense here.) Blogging tools such as Movable Type have implemented this standard by inserting the nofollow attribute in links in comments and TrackBacks. This link would not boost my PageRank even a smidgen:
That means comment spammers and referral spammers won't get rewarded for their nefarious activities on websites that implement nofollow. So, is the problem solved? Maybe. Partially. But ultimately? Not in my view. Here's why:
nofollow will never reach 100% adoption, so there will always be some incentive (even if it's decreased) to spam.
Spammers have shown that they do not care whether their techniques are effective in specific so long as they are effective in general. I have never published my referrer logs and referrer spammers have no real reason to hit my site, yet they do. They are targeting the blogosphere, not my site. Thus, as long as the blogosphere remains even partially vulnerable to referral spamming, it will continue. (Mark Pilgrim
agrees with me here.)
The resources required to fight spam, especially referral spam, so far oustrip the resources required to create it that nofollow is not a strong enough disincentive.
To expand upon point #3, consider just how easy it is to create referral spam. It's far easier than comment spam—there are myriad tools in MT, WP, and other publishing systems to combat this nuisance, so comment spam is not nearly as simple an enterprise as it used to be. It's also simpler to create than spam e-mail (and most e-mail users are protected by at least some sort of spam filter; logfiles are not). Referral spam is one HTTP request. The client need not acknowledge the response. It need not send anything but a simple packet with formatted text.
Here, why don't you spam Google for fun: go to wannaBrowser, enter Google.com as the Location, and enter anything (how about "This is referral spam!") as the Referrer. Voila! You sent referral spam to Google. Amazing.
Moral of the story: Since rel="nofollow" is not a panacea, bloggers are still going to get referrer spam.
Recommended webmaster practices
Referer spam is a problem because spammers can improve their sites' Google PageRank by getting listed on popular sites through spoofing of the HTTP_REFERER field in an HTTP request. (Jay Allen has suggested in the past that referral spammers want clickthroughs, but in an e-mail exchange, now agrees that they probably do it for the PageRank. Still, clickthroughs could be part of the equation, given that so many of these spamming sites are shut down quickly by their hosts.)
Best practice #1: Don't publish your referrers
If bloggers (and other website maintainers) did not publish this information, spammers would not bother to send these spoofed requests to blogs—it would be pointless. (For a humorous example, check out a blog entry on this very subject that's actually being targeted by pr0n site referral spammers.) Therefore, I propose that bloggers discontinue this practice. Others agree. I, for one, have never clicked on a link published in a blog's "Sites referring to me" (or similar) section. I think many bloggers simply believe this is a neat feature and have not evaluated its detrimental effect on the blogosphere as a whole.
Best practice #2: If you must publish referrers, include the page in robots.txt
If you're married to the idea of publishing referrers, you might want to try dasBlog 1.7, which looks to have built-in support for a referral spam blacklist. Also, take note on this great idea from Dave Winer (of Radio UserLand fame):
Winer says, "A couple of weeks ago we finally figured out why porn sites add themselves to referer pages on high page-rank sites: to improve their placement in search engines. Last night at dinner Andrew Grumet came up with the solution. In robots.txt specifically tell Googlebot and its relatives to not index the Referers page. Then the spammers won't get the page-rank they seek."
Grumet's idea is echoed in a recommendation for b2evolution users. Of course, this works only if you publish your referrers separates from the rest of site's content. If it's embedded, robots.txt can't help.
Best practice #3: Rob spammers of PageRank with rel="nofollow"
With the introduction several weeks ago of rel="nofollow", you can also rob the spammers of PageRank at the link-level, not just the page-level using robots.txt. All links referrer section of your website linking to externals websites should carry the rel="nofollow" attribute, without question.
Best practice #4: Gather a cleaner list of referrers using JavaScript and beacon images
As detailed by Marcel Bartels, referrer statistics gathered from beacon images loaded via JavaScript document.write statements are far more trustworthy than what the raw web server logs will contain. You may choose to disregard the referrers section of your server logs altogether and rely wholly on beacon images for referrer stats.
The .htaccess arms race is unwinnable
Referrer spammers are becoming more clever. They're registering odd- or innocuous-sounding domains that redirect to the "mothership"—sites with names like houseofsevengables.com (not teen-fetish-sex.com) that are difficult for a human to distinguish from a legitimate website. It's especially difficult because bloggers like to pick odd-sounding domains for their websites anyway. (For some fascinating speculation about referrer spam, see Nikke Lindqvist's post, "Referral spammers - why are they doing it and what should we do about them?")
In response to the ever-growing problem, many bloggers, including me, have begun fighting an unwinnable war with the referer spammers at the .htaccess level with mod_rewrite. (Some have even taken steps to automate this, such as with Referrer Spam Fucker 3000 or homebrew scripts).
But it's not working. Take a look the following:
My current .htaccess rules to block referral spamming websites. There are 58 separate lines of RewriteCond.
he Referring Sites section of the Analog report for underscorebleach.net for 13 January 2005.
There are legitimate referrers in the file, but many illegitimate sites as well. Can you tell at a glance which are which? Not without visiting them... and that's a problem. The .htaccess grows, and the spam still comes in. And more RewriteCond's = greater chance of false positives.
Moral of the story: The arms race of .htaccess blocking is unwinnable.
Technical characteristics of referral spam
I started to look at the individual HTTP request made by the referral spammers. Here's an example:
216.204.237.7 - - [13/Jan/2005:01:58:00 -0800] "GET /mt/mt-spameater.cgi?entry_id=764 HTTP/1.0" 200 5472 "http://www.paramountseedfarms.org/" "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.2; .NET CLR 1.1.4322)"
The spammer has taken pains to make the request look legitimate. The user_agent string looks very much like MSIE. (Interestingly, the request is in HTTP/1.0. Perhaps one could write a rule to exclude logfile entries in HTTP/1.0 with referrers. Normally, spiders that use HTTP/1.0 do not pass a referrer. Someone else more knowledgable than me would need to verify this.)
Also, it's not as if all of the referral spamming is coming from the same IP or set of IPs. Someone is commanding a large set of zombies here. (About a year ago, juju.org tackled the referral spamming problem with some nice directions and found that the spam was coming from a single IP, but I believe things have gotten more complex since then.) I trawled through the logfile for 13 January 2005 for eight random referrer spams and found eight different IPs.
BUT what is special about the request from the referrer spammer is that his IP is probably blacklisted somewhere. Now, as long as we follow through on the recommendation above to stop publishing referrers, there's no need to try and block the request in real-time. Besides, this would be a waste of resources and would hurt the 99% of users who are legitimate. However, we can query blacklists, such as through Distributed Sender Blackhole List (DSBL), at logfile analysis time to filter out the referral spam.
Also see Nikke Lindqvists's technical analysis of referral spam.
Idea #1: Filter referrer URLs against Jay Allen's MT-Blacklist
Previously on this site, I have criticized MT-Blacklist. That doesn't mean I don't think Jay Allen has done great work, and the current blacklist is a masterpiece—when used properly.
In this situation, I believe the blacklist could be a powerful, efficient weapon against referrer spam. See the current master blacklist file and compare it against my .htaccess rules, for example. Sites like houseofsevengables.com and canadianlabels.net are listed in the master blacklist file.
Therefore, if a logfile analysis program was to filter referrers against this list, it would certainly help root out spam. Also, the master blacklist is a simple text file that can be downloaded from a website (and also easily mirrored). It seems to me that this idea could be easily implemented. In fact, Omar Shahine has already written a .NET class to filter URLs against the MT-Blacklist.
The master blacklist isn't perfect, however, and a quick check of the file against the referrers that got through on 13 January 2005 shows that few or none of them were listed. That's why we should also consider Idea #2.
Another interesting development to note in this area is the Manila Referrer Spam Blacklist (MRSB). It seems to still be in the experimental stage at this point, but its XML-RPC approach is interesting. It would be fairly trivial to write plugins for popular blogging software allowing users to contribute spamming URLs to the MRSB database. The trick, I believe, would be in the vetting process. Right now I don't see that one exists (or I just don't understand it).
UPDATE 1/21/05: The idea is starting to catch fire. (Perhaps I originally posted this entry in one of those rare times when a few people are thinking about the same problem and arrive at the type of solution via multiple paths.) In any case, Tony at juju.org has developed the derefspam.pl Perl script to filter log files against Jay Allen's blacklist. In a similar vein, Rod at Groovy Mother wrote a patch for AWStats to do the same thing. Mark McLaughlin has followed suit for Shaun Inman's Shortstat. Great work!
UPDATE 2/1/05: Peter Wood has extended this idea to mod_security, writing the Perl script blacklist_to_modsec to combine the Jay Allen's blacklist and web server-level spam control. This goes "beyond the blog," baby. Nice.
Idea #2: Filter referrer IPs against spam blacklists
I recently implemented Brad Choate's MT-DSBL, a plugin that checks a commenter's IP against the blacklist maintained at DSBL (a service that keeps a list of open relays). I believe the general idea of combatting comment spam by harnessing the DSBL or DNS-based blackhole lists could also be used to ferret out referral spam.
I queried eight randomly selected referrer spamming IPs against OpenRBL.org. This website queries 28 blacklists and returns a Positive/Negative score, with a "positive" indicating that the IP is listed on the given blacklist.
#
IP address
Positive/Negative
1
66.237.84.20
4/24
2
213.172.36.62
0/28
3
61.9.0.99
1/27
4
68.47.42.60
7/21
5
203.162.3.77
1/27
6
193.188.105.16
2/26
7
213.56.68.29
7/21
8
200.242.249.70
7/21
The above table's scores are as of evening, 13 January 2005 (CST). They may be different if you check an IP's blacklist presence now.
My proposal for log file filtering of referrer spamming is rather simple:
For a request with a referrer, query the IP against a blacklist. This might be DSBL or another list. I'm certainly not the best one to decide.
If the IP is blacklisted (or has a high score among a multitude of blacklists), refrain from listing that referring URL in any section of a site's Web stats.
Once a given site has been identified as a referral spam hostname (e.g. houseofsevengables.com, as mentioned above), do not bother querying the blacklist again for any IPs with this hostname in the HTTP request. This is simply for efficiency's sake.
Once an IP has been identified as a referral spamming IP, do not bother querying the blacklist again. Again, efficiency's sake.
UPDATE 1/16/05: Also, Chris Wage has written up a great set of directions for using the mod_access_rbl module in Apache to match IPs against DNSBLs. While this won't catch 100% of referral spamming IPs (see the variation in scores above), it should cut down on the number that get through. You might wonder what sort of affect this method would have on site performance. Here's Chris' response:
Response time is affected, but not much for normal usage. The query responses are cached by [your] local nameserver on the same network, so the most someone would notice is a slight delay on the first load of the page.
Conclusion
Referral spam will not go away until bloggers make it a useless enterprise for spammers. Spammers are not stupid, and they will gradually stop the practice if they see that their efforts have no return.
In the meantime, I propose the above methods for filtering the referrer stats of websites. This is performed at logfile analysis time, not when the HTTP request is made. It seems to me that Ideas #1 and #2 could be combined, with #1 more efficient for client and server and #2 more likely to be up-to-date in real-time.
I welcome all comments. I am certainly no expert in these matters, but in searching the Web, I have found a lack of discussion in this area.
Addenda
It's also been suggested that Web stat scripts could check the referrer's website for a link back to one's website. If no link is found, the script would assume that the site is a bogus, spam URL. I see two problems with this approach:
Blog indexes change quickly. What's on the index page at 2:00 p.m. might be gone at 2:30 p.m. This can be because the blogger deletes the link to your site or because the index "rolls over" and displays only the past 10 entries.
Spammers could quickly adapt. They could simply link to every site they spam.
If you do use the .htaccess method to combat referrer spam, I suggest wannaBrowser to test your rewrite rules. It's the simplest way to see whether you're properly blocking spam URLs. The htaccess blocking generator will help you write the rules. SixDifferentWays has a pretty complete post on battling the spam this way. Ed Costello's article is also good.
Other resources
CentreBlog's background on referrer spam
Tao of Mac's "Referrer Spam Should Be a Crime"

Sunday, October 17, 2004

 

Message Queue

1.
By Rickie
From http://www.cnblogs.com/rickie/archive/2004/11/16/64345.html
http://www.cnblogs.com/rickie/archive/2004/11/17/64712.html

利用 MSMQ(Microsoft Message Queue),应用程序开发人员可以通过发送和接收消息方便地与应用程序进行快速可靠的通信。消息处理为您提供了有保障的消息传递和执行许多业务处理的可靠的防故障方法。

MSMQ与XML Web Services和.Net Remoting一样,是一种分布式开发技术。但是在使用XML Web Services或.Net Remoting组件时,Client端需要和Server端实时交换信息,Server需要保持联机。MSMQ则可以在Server离线的情况下工作,将Message临时保存在Client端的消息队列中,以后联机时再发送到Server端处理。

显然,MSMQ不适合于Client需要Server端及时响应的这种情况,MSMQ以异步的方式和Server端交互,不用担心等待Server端的长时间处理过程。

虽然XML Web Services和.Net Remoting都提供了[OneWay]属性来处理异步调用,用来解决Server端长方法调用长时间阻碍Client端。但是不能解决大量Client负载的问题,此时Server接受的请求快于处理请求。

一般情况下,[OneWay]属性不用于专门的消息服务中。

1. 基本术语和概念(Basic terms and concepts)

“消息”是在两台计算机间传送的数据单位。消息可以非常简单,例如只包含文本字符串;也可以更复杂,可能包含嵌入对象。

消息被发送到队列中。“消息队列”是在消息的传输过程中保存消息的容器。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递;如果发送消息时接收者不可用,消息队列会保留消息,直到可以成功地传递它。

“消息队列”是 Microsoft 的消息处理技术,它在任何安装了 Microsoft Windows 的计算机组合中,为任何应用程序提供消息处理和消息队列功能,无论这些计算机是否在同一个网络上或者是否同时联机。

“消息队列网络”是能够相互间来回发送消息的任何一组计算机。网络中的不同计算机在确保消息顺利处理的过程中扮演不同的角色。它们中有些提供路由信息以确定如何发送消息,有些保存整个网络的重要信息,而有些只是发送和接收消息。

“消息队列”安装期间,管理员确定哪些服务器可以互相通信,并设置特定服务器的特殊角色。构成此“消息队列”网络的计算机称为“站点”,它们之间通过“站点链接”相互连接。每个站点链接都有一个关联的“开销”,它由管理员确定,指示了经过此站点链接传递消息的频率。

“消息队列”管理员还在网络中设置一台或多台作为“路由服务器”的计算机。路由服务器查看各站点链接的开销,确定经过多个站点传递消息的最快和最有效的方法,以此决定如何传递消息。

2. 队列类型(Queue Type)

有两种主要的队列类型:由您或网络中的其他用户创建的队列和系统队列。

用户创建的队列可能是以下任何一种队列:

“公共队列”在整个“消息队列”网络中复制,并且有可能由网络连接的所有站点访问。

“专用队列”不在整个网络中发布。相反,它们仅在所驻留的本地计算机上可用。专用队列只能由知道队列的完整路径名或标签的应用程序访问。

“管理队列”包含确认在给定“消息队列”网络中发送的消息回执的消息。指定希望 MessageQueue 组件使用的管理队列(如果有的话)。

“响应队列”包含目标应用程序接收到消息时返回给发送应用程序的响应消息。指定希望 MessageQueue 组件使用的响应队列(如果有的话)。

系统生成的队列一般分为以下几类:

“日记队列”可选地存储发送消息的副本和从队列中移除的消息副本。每个“消息队列”客户端上的单个日记队列存储从该计算机发送的消息副本。在服务器上为每个队列创建了一个单独的日记队列。此日记跟踪从该队列中移除的消息。

“死信队列”存储无法传递或已过期的消息的副本。如果过期或无法传递的消息是事务性消息,则被存储在一种特殊的死信队列中,称为“事务性死信队列”。死信存储在过期消息所在的计算机上。有关超时期限和过期消息的更多信息,请参见默认消息属性。

“报告队列”包含指示消息到达目标所经过的路由的消息,还可以包含测试消息。每台计算机上只能有一个报告队列。

“专用系统队列”是一系列存储系统执行消息处理操作所需的管理和通知消息的专用队列。

在应用程序中进行的大多数工作都涉及访问公共队列及其消息。但是,根据应用程序的日记记录、确认和其他特殊处理需要,在日常操作中很可能要使用几种不同的系统队列。

3. 同步和异步通信(Synchronous VS. Asynchronous Communication)

队列通信天生就是异步的,因为将消息发送到队列和从队列中接收消息是在不同的进程中完成的。另外,可以异步执行接收操作,因为要接收消息的人可以对任何给定的队列调用 BeginReceive 方法,然后立即继续其他任务而不用等待答复。这与人们所了解的“同步通信”截然不同。

在同步通信中,请求的发送方在执行其他任务前,必须等待来自预定接收方的响应。发送方等待的时间完全取决于接收方处理请求和发送响应所用的时间。

4. 同消息队列交互(Interacting with Message Queues)

消息处理和消息为基于服务器的应用程序组件之间的进程间通信提供了强大灵活的机制。同组件间的直接调用相比,它们具有若干优点,其中包括:

稳定性 — 组件失败对消息的影响程度远远小于组件间的直接调用,因为消息存储在队列中并一直留在那里,直到被适当地处理。消息处理同事务处理相似,因为消息处理是有保证的。
消息优先级 — 更紧急或更重要的消息可在相对不重要的消息之前接收,因此可以为关键的应用程序保证足够的响应时间。
脱机能力 — 发送消息时,它们可被发送到临时队列中并一直留在那里,直到被成功地传递。当因任何原因对所需队列的访问不可用时,用户可以继续执行操作。同时,其他操作可以继续进行,如同消息已经得到了处理一样,这是因为网络连接恢复时消息传递是有保证的。
事务性消息处理 — 将多个相关消息耦合为单个事务,确保消息按顺序传递、只传递一次并且可以从它们的目标队列中被成功地检索。如果出现任何错误,将取消整个事务。
安全性 — MessageQueue 组件基于的消息队列技术使用 Windows 安全来保护访问控制,提供审核,并对组件发送和接收的消息进行加密和验证。

5. 在.Net环境下编写简单的Message Queue程序

(1)先安装Message Queuing Services

通过Control Panel,“Add/Remove Programs” – “Add/Remove Windows Components”步骤安装MSMQ。

MSMQ可以安装为工作组模式或域模式。如果安装程序没有找到一台运行提供目录服务的消息队列的服务器,则只可以安装为工作组模式,此计算机上的“消息队列”只支持创建专用队列和创建与其他运行“消息队列”的计算机的直接连接。

(2)配置MSMQ

打开Computer Management – Message Queuing,在Private Queues下创建MSMQDemo队列

(3)编写代码-简单演示MSMQ对象

MessageQueue 类是“消息队列”周围的包装。MessageQueue 类提供对“消息队列”队列的引用。可以在 MessageQueue 构造函数中指定一个连接到现有资源的路径,或者可在服务器上创建新队列。在调用 Send、Peek 或 Receive 之前,必须将 MessageQueue 类的新实例与某个现有队列关联。

MessageQueue 支持两种类型的消息检索:同步和异步。同步的 Peek 和 Receive 方法使进程线程用指定的间隔时间等待新消息到达队列。异步的 BeginPeek 和 BeginReceive 方法允许主应用程序任务在消息到达队列之前,在单独的线程中继续执行。这些方法通过使用回调对象和状态对象进行工作,以便在线程之间进行信息通讯。

// Send Message

private void btnSendMessage_Click(object sender, System.EventArgs e)

{

// Open queue

System.Messaging.MessageQueue queue = new System.Messaging.MessageQueue(".\\Private$\\MSMQDemo");

// Create message

System.Messaging.Message message = new System.Messaging.Message();

message.Body = txtMessage.Text.Trim();

message.Formatter = new System.Messaging.XmlMessageFormatter(new Type[] {typeof(string)});

// Put message into queue

queue.Send(message);

}

// Receive Message

private void btnReceiveMessage_Click(object sender, System.EventArgs e)

{

// Open queue

System.Messaging.MessageQueue queue = new System.Messaging.MessageQueue(".\\Private$\\MSMQDemo");

// Receive message, 同步的Receive方法阻塞当前执行线程,直到一个message可以得到

System.Messaging.Message message = queue.Receive();

message.Formatter = new System.Messaging.XmlMessageFormatter(new Type[] {typeof(string)});

txtReceiveMessage.Text = message.Body.ToString();

}

这里归纳在.Net 环境下应用消息队列(MSMQ)开发的一些基本对象和方法。

队列类型及其相应的路径格式:

Public: [MachineName]\[QueueName]

Private: [MachineName]\Private$\[QueueName]

Journal: [MachineName]\[QueueName]\Journal$

Machine journal: [MachineName]\Journal$

Machine dead-letter: [MachineName]\DeadLetter$

Machine transactional dead-letter: [MachineName]\XactDeadLetter$

The first portion of the path indicates a computer or domain name or uses a period (.) to indicate the current computer.

1. 创建消息队列

可以手动的方式通过Windows提供的工具创建,或者通过程序的方式创建:

if(MessageQueue.Exists(".\\Private$\\MSMQDemo"))

queue = new MessageQueue(".\\Private$\\MSMQDemo");

else

queue = MessageQueue.Create(".\\Private$\\MSMQDemo");

2. 发送消息

缺省情况下,消息序列化XML格式,也可设置为MessageQueue对象的Formatter属性为BinaryMessageFormatter,以二进制格式序列化。

设置消息序列化格式:

if(rdoXMLFormatter.Checked)

queue.Formatter = new XmlMessageFormatter();

else

queue.Formatter = new BinaryMessageFormatter();

发送简单的文本消息:

string strMessage = "Hello, I am Rickie.";

queue.Send(strMessage, "Simple text message");

消息队列可以传送简单的文本消息,也可以传送对象消息,但需要满足如下条件:

(1)class必须有一个无参数的公共构造函数,.Net使用这个构造函数在接收端重建对象。

(2)class必须标示为serializable(序列化)。

(3)所有的class属性必须可读写,因为.Net在重建对象时不能够恢复只读属性的属性值,因此只读属性不能够序列化。

发送对象消息(CustomerInfo class需要满足上述条件):

CustomerInfo theCustomer = new CustomerInfo("0001", "Rickie Lee", "Rickieleemail@yahoo.com");

queue.Send(theCustomer, "Object message");

3. 读/显示消息

当消息接受后,消息将从队列中删除。可以通过使用MessageQueue.Peek方法来检索消息队列中的第一个消息的复制,保留消息在队列中。不过,这样只能获取的相同的消息。更好的办法是通过foreach来读消息队列中的消息,但不删除队列中的消息。

foreach(System.Messaging.Message message in queue)

{

txtResults.Text += message.Label + Environment.NewLine;

}

4. 接收消息

一般而言,可以通过Receive方法来读取队列中的消息,对于非事务性的队列,优先读取高优先级的消息。如果队列中有多个相同优先级的消息,则以先进先去的方式进行读取消息。对于事务性的队列,则完全以先进先去的方式进行读取消息,忽略消息的优先级。

System.Messaging.Message receivedMessage;

receivedMessage = queue.Receive(TimeSpan.FromSeconds(5));

上面采用同步调用,并且一直等到队列中有可用消息或超时过期。

其他相关事项:

关于消息的加密、路由等等特性,需要有配置Active Directory的消息队列服务器。
为了避免存放消息队列的计算机重新启动而丢失消息,可以通过设置消息对象的Recoverable属性为true,在消息传递过程中将消息保存到磁盘上来保证消息的传递,默认为false。
消息发送方和消息接收方需采用相同的序列化格式,如XML或Binary。
建议每一个消息队列存放相同类型的消息对象,这样可以省掉获取消息对象后,进行类型判别的麻烦。
5.消息队列在分布式系统中的应用

消息队列MSMQ和数据库不一样,消息队列缺乏足够的错误检查能力,并且MSMQ由于需要束缚在windows平台,这些是MSMQ的不足之处。另外,在Production环境中,需要编写大量的代码来进行错误检测和响应。还有大量的死信队列、响应队列和日记队列可能部分在企业不同的计算机上,使得跟踪这些问题或进行诊断变得比较困难。

但是,MSMQ作为组件内部连接比较有用。例如,你可以创建一个XML Web Services使用MSMQ来转发对另一个Server端组件的请求,这种设计巧妙回避了其他异步调用的方法,并且确保可扩展性和性能。

References:

Matthew MacDonald, Microsoft® .NET Distributed Applications: Integrating XML Web Services and .NET Remoting

Saturday, October 16, 2004

 

Working with Client-Side Script

1.
Working with Client-Side Script

By Scott Mitchell
From
http://msdn.microsoft.com/asp.net/using/building/web/default.aspx?pull=/library/en-us/dnaspp/html/clientsidescript.asp

Chinese version

Translated By hiyaolee
From http://blog.csdn.net/hiyaolee/archive/2004/10/14/136899.aspx

摘要:尽管 ASP.NET 在服务器上执行其大多数操作,但是某些操作在客户端进行处理可能会更好。Scott Mitchell 说明了 ASP.NET 页面和控件如何添加客户端代码。



本页内容
简介
创建基类作为添加客户端脚本的基础
从代码隐藏类添加客户端脚本
根据对用户操作的响应执行客户端代码
实现常用客户端功能
小结
相关书籍

简介
当使用动态的、基于 Web 的脚本技术时,与传统 ASP 或 PHP 类似,开发人员必须对客户端和服务器间的逻辑、暂时和物理分隔有着敏锐的理解。例如,对于触发服务器端代码执行的用户操作,使用传统 ASP 的开发人员必须明确地使用户的浏览器将请求返回到 Web 服务器。创建这样的交互可能会轻易地占用大量开发时间,并且导致不易读的代码。

Microsoft ASP.NET 通过使用 Web 窗体,有助于减轻将用户事件绑定到特定服务器端代码执行的负担,这就模糊了客户端和服务器间的界线。使用 ASP.NET 和最少的工作,开发人员就可以快速地创建如下的网页,它具有大量的交互式用户界面元素按钮、下拉列表等,而这些都基于最终用户的操作,可以选择性地运行服务器端代码。例如,利用 ASP.NET 添加下拉列表,只要选定的下拉列表项目更改则执行某些操作,您只需添加 DropDownList Web 控件、将其 AutoPostBack 属性设置为 True,然后为该下拉列表创建一个 SelectedIndexChanged 事件处理程序。如果利用传统的 ASP 完成上述任务,则会要求编写许多复杂的 HTML、客户端 JavaScript 和服务器端脚本代码;利用 ASP.NET,则为您提供了必要的脚本代码和服务器端事件模型。

尽管在执行客户端操作时,ASP.NET 中的 Web 窗体极大地简化了运行服务器端脚本,但是,如果误用这种功能可能会导致无法接受的性能。尽管 Web 窗体隐藏了所涉及的复杂性,每次需要执行服务器端代码时,最终用户的浏览器必须通过重新提交窗体,将请求返回到 Web 服务器。当提交窗体时,所有窗体字段(文本框、下拉列表和复选框等)必须同时返回它们的值。此外,页面的视图状态也被返回到 Web 服务器。总而言之,每次回发网页时,几千字节的数据将需要潜在地发送回 Web 服务器。于是,经常回发可能很快就会导致 Web 应用程序不可使用,尤其是对于那些仍然使用拨号连接的用户。通过将功能推到客户端可以降低经常回发的需要。

注 ASP.NET Web 窗体发出一个标题为 VIEWSTATE 的隐藏窗体字段,它包含 Web 窗体中 Web 控件已更改状态的基于 64 位编码的表示。根据出现的 Web 控件,视图状态的范围可以从几十字节到几万字节。要学习有关视图状态的更多知识,请查阅我的文章 Understanding ASP.NET View State。

http://msdn.microsoft.com/library/en-us/dnaspp/html/viewstate.asp

利用传统的 ASP,添加数据驱动、自定义客户端脚本非常简单,但并不是非常易读。例如,要在传统的 ASP 中显示根据某个 ID 字段加载 URL 的弹出窗口,您可以使用 语法来插入 ID 字段的值,在适当的客户端脚本中进行输入。ASP.NET 允许您利用 Page 类中的各种方法,创建这种数据驱动的客户端脚本。

本文分析了向 ASP.NET 网页添加客户端脚本的技术。客户端脚本是运行在访问者浏览器中的脚本代码,如其名字所示。我们将看到如何完成常见的客户端任务,例如显示警告、确认框和弹出窗口。(客户端脚本窗体字段验证的一个主要用途可能与 ASP.NET 主题有点不相关,因为验证程序 Web 控件提供了随取随用的客户端窗体验证。)本文的重点在于插入客户端脚本的服务器端类、方法和技术;我们不会详细地分析实际的客户端脚本,因为该信息涉及了围绕 Web 的众多其他文章和站点。

返回页首
创建基类作为添加客户端脚本的基础
传统的 ASP 和 ASP.NET 之间的主要差别之一在于各自技术的编程模型。ASP 页面是原子的、程序上的脚本,解释每个页面的访问。然而,ASP.NET 完全是面向对象的编程技术。所有 ASP.NET 网页都是带有属性、方法和事件的类。所有网页直接或间接地派生自 System.Web.UI 命名空间中的 Page 类,Page 类包含了 ASP.NET 网页的基本功能。

面向对象编程的概念之一就是继承。继承使您可以创建一个扩展其他类功能的新类。(如果类 B 继承类 A,也可以说扩展了 A;类 A 被称为基类。)当使用代码隐藏模型来创建 ASP.NET 网页时,可以非常清楚地看到代码隐藏类继承了 Page 类:

Public Class WebForm1
Inherits System.Web.UI.Page

...
End Class

通过使您的代码隐藏类继承 Page 类,它自动接收在 Page 类中继承的功能,例如 Request、Response、Session 和 ViewState 对象以及常见事件,如 Init、Load、Render 等等。我们将在本文中看到,如果您需要可用于所有 ASP.NET 网页的某个常见功能,一种方法是创建派生自 Page 类并具有完成这些所需增强功能的其他方法和属性的类。然后,要使 ASP.NET 网页利用这些增强功能,我们只需更新页面代码隐藏类中的 Inherits 语句,以使用扩展 Page 类的类。

在本文中,我们将创建一个名为 ClientSidePage 的类,它派生自 Page 类并提供额外的方法以协助执行常见的客户端任务。通过让代码隐藏类继承 ClientSidePage,而不是继承 Page,添加脚本代码将会像调用方法和传送几个参数那样简单。具体说来,该类包括用于下列用途的方法:

• 显示模式客户端对话框。

• 在页面加载时将焦点设置到特定的窗体字段。

• 使用模式确定对话框来确定用户是否希望回发该窗体。

• 显示弹出窗口。


在我们深入研究 ClientSidePage 类之前,首先让我们分析一下 Page 类中的有关方法,以便将客户端脚本插入到网页中。在我们讨论这些 Page 方法后,我们将开始利用 ClientSidePage 类扩展它们的功能,并且查看如何将所有内容结合在一起以及如何在 ASP.NET 网页中使用扩展的类。

返回页首
从代码隐藏类添加客户端脚本
所有 ASP.NET 网页必须直接或间接地派生自 System.Web.UI 命名空间中的 Page 类。Page 类包含正常运行的网页所要求的方法、属性和事件的基本集合。在该类的众多方法中,一些方法旨在将客户端脚本插入到生成的 HTML 中。这些方法从代码隐藏类调用,因此可以用于发出数据驱动的客户端脚本。用于发出客户端脚本的相关 Page 类方法如下所示。

该基类派生自 System.Web.UI.Page 类,因此可以通过从代码隐藏类直接调用 Page 类的公共方法来访问它们。

注 要访问 Page 类的方法,您可以直接键入方法名,或者通过输入 MyBase.(对于 Microsoft Visual Basic .NET)、this. (对于 C#)或者 Page.(对于 C# 或 Visual Basic .NET),利用 Microsoft Visual Studio .NET 中的 IntelliSense 来实现。如果使用 Visual Basic .NET 作为选择的编程语言,请确保将 Visual Studio .NET 配置为不 隐藏高级成员,否则将无法看到这些客户端脚本方法。(要显示高级成员,请转到 Tools | Options | TextEditor | Basic,然后清除 Hide advanced members 复选框。)

RegisterClientScriptBlock(key, script)
在 Web 窗体已呈现的 form> 元素之后,在包含于 Web 窗体中的任意 Web 控件之前,RegisterClientScriptBlock 方法会添加一个客户端脚本块。key 输入参数允许您指定与该脚本块相关联的唯一的密钥,而 script 参数包括要发出的完整的脚本代码。(这个 script 参数应该包括实际的 script> 元素,同时还包括客户端 JavaScript 或 Microsoft VBScript。)

在通过 ASP.NET 网页的代码隐藏类发出客户端脚本时,通常情况下,key 参数的值就不是非常重要了。简单地选择一个说明性的密钥值。在通过自定义编译的服务器控件插入客户端脚本代码时,key 参数就更加适用。编译的控件有可能需要一组客户端函数。一个页面上服务器控件的多个实例可以共享这些公用客户端脚本函数,因此对于整个页面而言,这些函数只需要发出一次即可,不需要每个控件实例发送一次。例如,验证控件利用客户端代码来增强用户体验。如果页面上存在任意验证控件,这种客户端代码就必须存在,但是如果存在多个验证控件,那么全部控件都可以使用这个单个的共享函数的集合。

通过为脚本块提供一个密钥,利用公用客户端函数集合构建控件的控件开发人员可以检查所要求的公用函数集合是否已经被页面上控件的另一个实例添加。如果已添加,它不需要重新添加公用脚本。要检查脚本块是否已经使用相同的密钥添加,请使用 IsClientScriptBlockRegistered(key) 方法,它将返回布尔值,表示带有相同密钥的脚本块是否已经进行了注册。需要注意的是可以在不首先检查它是否注册的情况下添加脚本块。如果尝试利用已经注册的密钥添加脚本块,添加的脚本块将被忽略,并且原来的脚本将保持分配到该密钥。

注IsClientScriptBlockRegistered 方法在以下两种情况下尤为有用。第一,当添加相似但又独特的脚本块时它很方便,您需要确保每个脚本块都给予唯一的密钥。本文稍后分析的代码说明了“is registered”方法的实用性。第二个用途就是当构建需要某个公用脚本的控件时,尤其是如果特别的生成该脚本。通过使用 IsClientScriptBlockRegistered 方法,可以确保对页面上服务器控件的所有脚本通用的脚本在每次页面加载时只生成一次,而不是对页面上的每个控件实例都生成一次。

RegisterClientScriptBlock 方法对于添加客户端脚本非常有用,该脚本不依赖于 Web 窗体内出现的任意窗体字段。该方法的常见使用就是显示客户端警告框。例如,设想您具有一个带有一些 TextBox Web 控件和一个“Save”按钮的网页。TextBox 控件可能会具有来自数据库的特殊值。设想该页面允许用户修改这些值并通过单击“Save”按钮提交他们所做的更改。当单击“Save”时,网页将会回发,并且会触发按钮的 Click 事件。您可以为更新数据库的事件创建一个服务器端事件处理程序。要使用户知道他们的更改已经保存,您可能希望显示一个警告框“Your changes have been saved”。通过将以下代码行添加到按钮的 Click 事件处理程序中,可以实现这个任务:

RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript""> _
alert('Your changes have been saved.'); _
/script>")

上述代码将会在页面的 form> 内添加指定的脚本内容,但是在该窗体的内容前。当在用户浏览器中生成页面时,他们将会看到根据页面加载显示的客户端警告框,如图 1 所示。

form method="post" ...>
script language="JavaScript">
alert('Your changes have been saved.');
/script>
...
/form>



图 1. 客户端 JavaScript 的结果

注 上面示例中一个潜在地不需要的副作用就是,警告框将会在浏览器接收到 form> 标记后立即显示。在用户单击警告框的“OK”按钮之前,浏览器将挂起网页的呈现。这意味着用户在单击“OK”之前,将看到一个空白的浏览器页面。如果希望在显示警告框之前完全显示该页面,您可以使用 RegisterStartupScript 方法(我们将在下面进行讨论),在 form> 元素的结尾处插入 JavaScript。

RegisterStartupScript(key, script)
RegisterStartupScript 方法与 RegisterClientScriptBlock 方法非常相似。主要的区别在于发出客户端脚本的位置。在 form> 元素开始后,在窗体的内容前,记住用 RegisterClientScriptBlock 发出脚本。另一方面,RegisterStartupScript 在窗体的结尾 处、在所有窗体字段后,添加指定的脚本。使用 RegisterStartupScript 来放置与呈现的 HTML 元素交互的客户端脚本。(稍后我将研究根据页面加载将焦点设置到窗体字段的示例;要完成这个操作,您将要使用 RegisterStartupScript 方法。)

与 RegisterClientScriptBlock 相似,由 RegisterStartupScript 添加的脚本块需要一个唯一的密钥值。同样,该密钥值主要由自定义控件开发人员使用。并不奇怪,还有一个 IsStartupScriptRegistered(key) 方法,它返回布尔值,表示带有指定密钥 的脚本块是否已经进行了注册。

注 有关使用 RegisterStartupScript 和 RegisterClientScriptBlock 来创建自定义编译的服务器控件的详细信息,请阅读我以前的文章: Injecting Client-Side Script from an ASP.NET Server Control.

RegisterArrayDeclaration(arrayName, arrayValue)
如果需要创建带有某些设置值的客户端 JavaScript Array 对象,请使用该方法向特定的数组添加值。例如,当使用 ASP.NET 网页中的验证控件时,就会构建 Array 对象 (Page_Validators),以包含对页面上验证控件集合的引用。当提交窗体时,就会枚举该数组以检查各种验证控件是否有效。

要将值 1、2 和 3 添加到名为 FavoriteNumbers 的客户端 Array 对象,要使用以下服务器端代码:

RegisterArrayDeclaration("FavoriteNumbers", "1")
RegisterArrayDeclaration("FavoriteNumbers", "2")
RegisterArrayDeclaration("FavoriteNumbers", "3")

这段代码会发出以下客户端脚本:

script language="javascript">

/script>

请注意,被传递的每个数组值都必须是字符串;但是,呈现的客户端脚本将 Array 对象的值设置为字符串的内容。也就是说,如果希望创建带有字符串值“Scott”和“Jisun”的 Array,要使用以下代码:

RegisterArrayDeclaration("FavoriteFolks", "'Scott'")
RegisterArrayDeclaration("FavoriteFolks ", "'Jisun'")

请注意,第二个输入参数是包含 'Scott' 和 'Jisun' 的字符串 - 文本由单撇号分隔。这会显示以下客户端脚本:

script language="javascript">

/script>

RegisterHiddenField(hiddenFieldName, hiddenFieldValue)
在传统的 ASP 中,通常需要将各种信息从一个页面分发到另一个页面。完成这个操作的常用方法就是使用隐藏窗体字段。(隐藏窗体字段是不显示的窗体字段,但是它的值会根据窗体的提交而发送。创建隐藏窗体字段的语法是 。)在 ASP.NET 中,通过自定义隐藏窗体字段传递信息的需要会极大地减少,因为页面中的控件状态会自动保持。但是,如果您发现需要创建自定义隐藏窗体字段,可以通过 RegisterHiddenField 方法来完成。

RegisterHiddenField 方法接受两个输入参数。隐藏字段的名称和值。例如,要创建带有名称 foo 和值 bar 的隐藏窗体字段,请使用以下代码:

RegisterHiddenField("foo", "bar")

这会在页面的 form> 元素中添加隐藏窗体字段,如下所示:

form name="_ctl0" method="post" action="test.aspx" id="_ctl0">

...
/form>

理解客户端元素是如何呈现的
Page 类包含负责呈现在上面讨论的方法中注册的客户端脚本的两个 internal 方法:OnFormRender 和 OnFormPostRender。(标记为 internal 的方法只能被相同程序集中的其他类调用。因此,无法从 ASP.NET Web 应用程序中的代码隐藏类调用 Page 的 internal 方法。)这两个方法都在 HtmlForm 类的 RenderChildren 方法中进行调用。位于 System.Web.UI.HtmlControls 命名空间中的 HtmlForm 类表示 Web 窗体;也就是说,ASP.NET 网页中的服务器端窗体form runat="server">.../form> 在页面的实例化阶段中作为 HtmlForm 类的实例加载。

因为由 Page 类的众多方法注册的客户端脚本是在 OnFormRender 和 OnFormPostRender 方法中呈现的,并且因为这些方法只能由 HtmlForm 类进行调用,所以通过这些方法以编程方式添加的客户端脚本,只有在网页包含 Web 窗体时才会呈现。也就是说,通过上面讨论的任意方法以编程方式添加的任意脚本元素,在 ASP.NET 网页包含服务器端窗体(form runat="server">.../form>)时只会在页面的最后标记中发出。

通过首先添加开始 form> 元素,会呈现 ASP.NET 网页上的 Web 窗体。之后,会调用 Web 窗体的 RenderChildren 方法,它包含三行代码:

Page.OnFormRender(...)
MyBase.RenderChildren(...)
Page.OnFormPostRender(...)

对 Page 类的 OnFormRender 方法的调用会添加以下标记:

• 通过对 RegisterHiddenField 进行调用而添加的任意隐藏窗体字段。

• 隐藏窗体字段中名为 __VIEWSTATE 的基于 64 位编码的视图状态。

• 通过对 RegisterClientScriptBlock 进行调用而添加的任意脚本块。


Web 窗体的 RenderChildren 方法中的第二行代码调用基类的 RenderChildren 方法,它会在 Web 窗体中呈现内容。在呈现所有窗体的内容后,对 Page 类的 OnFormPostRender 方法进行调用,这将会添加以下客户端内容:

• 由 RegisterArrayDeclaration 方法添加的任意 Array 对象。

• 通过对 RegisterStartupScript 进行调用而添加的任意脚本块。


最后,在 Web 窗体的 RenderChildren 方法完成后,则会呈现关闭窗体标记 ()。图 2 以图表形式说明了这个呈现过程。

注 图 2 假设您有些熟悉 ASP.NET 页面的生命周期。如果您对学习更多有关页面生命周期的知识感兴趣,请考虑阅读 Understanding ASP.NET View State,将重点放在标题为“The ASP.NET Page Life Cycle”的节上。



图 2. ASP.NET 中呈现的页面

分析脚本块的呈现顺序
乍看 Page 类的注册方法,在网页中呈现的已注册元素的顺序好像是对应于它们被添加到代码中的顺序。也就是说,设想 ASP.NET 网页的代码隐藏类中有以下两行代码:

RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript"">var name='" & _
someDataDrivenValue & "'; /script>")
RegisterClientScriptBlock("showSaveMessage", _
"script language=""JavaScript"">alert('Hello, ' + name + '!');
/script>")

当发现页面呈现以下客户端脚本块(假设 someDataDriveValue 的值为 Sam)时,您不要太奇怪:

script language="JavaScript">var name='Sam';/script>
script language="JavaScript">alert('Hello, ' + name + '!');/script>

访问该页面的用户将会看到一个警告框显示“Hello, Sam!”。

基于这个测试,您可能会认为这样的事实始终成立:在 HTML 页面中发出脚本块的顺序就是在服务器端代码中为它们指定的顺序。但是,这是一个不正确的假设,并且可以导致页面中断。例如,设想前面添加的脚本块在 HTML 页面中以相反的顺序发出。那么,您将会得到:

script language="JavaScript">alert('Hello, ' + name + '!');/script>
script language="JavaScript">var name='Sam';/script>

这将会显示警告框“Hello, !”,因为变量 name 尚未分配值。显然,有些时候发出脚本块的顺序非常重要的。

Page 类的注册方法 - RegisterClientScriptBlock、RegisterStartupScript、RegisterArrayDeclaration 和 RegisterHiddenFields - 全部将提供的脚本内容写入到内部 HybridDictionary 中。HybridDictionary 是在 System.Collections.Specialized 命名空间中发现的数据结构,设计用于在有很多项目未知的字典中存储项目。对于小型的项目集合,ListDictionary 是最有效的数据结构,但是对于较大的字典,Hashtable 会更有效。HybridDictionary 分离差异 - 它通过使用 ListDictionary 存储项目来开始。当 ListDictionary 添加了它的第九个项目后,HybridDictionary 会从使用 ListDictionary 切换到使用 Hashtable。

尽管这种方法对于性能而言非常理想,但是如果您使用几个脚本块而且脚本块的顺序非常重要,那么它可能会带来严重的破坏。这是因为,ListDictionary 维护元素被添加的顺序,而 Hashtable 没有进行维护。因此,如果您将八个或更少项目添加到任意一个特定的注册方法中,项目将会以它们被添加的顺序发出。但是,如果添加了第九个项目,脚本发出的顺序看起来将是随机的。

注ListDictionary 使用 linked list 存储它的元素,而 Hashtable 将它的元素存储在数组中,该数组的内容按照字符串键的哈希值进行排序。有关链接列表和哈希表的完整讨论已经远远超出了本文所讨论的范围。有关详细信息,包括对其性能的分析,请考虑阅读 An Extensive Examination of Data Structures,尤其是 Part 2 和 Part 4。

如果您计划出现如下情况:可能会存在使用特殊注册方法添加的多于八个客户端元素,并且元素的出现顺序比较重要,那么您可能希望研究一下 Peter Blum 的免费的 RegisterScripts 库。对于所发出客户端元素的顺序,RegisterScripts 提供了更大的控制能力,并且还提供了无需手动添加 script> 标记的选项,当利用 RegisterClientScriptBlock 或 RegisterStartupScript 方法包括客户端脚本时,您必须添加此标记。

返回页首
根据对用户操作的响应执行客户端代码
对于插入页面加载时运行的客户端代码而言,Page 类的注册方法非常理想,但是在很多情况下,我们希望根据对最终用户操作的响应来运行代码。例如,我们可能希望当用户单击按钮时显示确认对话框,或者当下拉列表的选定项目发生变化时调用特殊的客户端 JavaScript 功能。

HTML 元素具有您可以点击的大量客户端事件,并且当触发事件时可以执行客户端代码。所要求的标记只是在 HTML 元素的标记中作为一个属性。例如,要在单击某个按钮时显示警告框,可以添加以下代码:

value="Click me to see an alert box!"
onclick="alert('Here it is!');" />

要在客户端事件触发时运行客户端代码,可以将适当的属性添加到 HTML 元素中。对于 Web 控件,可以借助于编程方式使用 Attributes 集合添加客户端属性。例如,设想您具有一个 TextBox Web 控件,只要呈现的文本框获得焦点,您就希望它突出显示为黄色。要完成上述操作,您要将 TextBox Web 控件的呈现 HTML 添加如下所示的代码:

onfocus="this.style.backgroundColor='yellow';"
onblur="this.style.backgroundColor='white';" />

要完成这个标记,我们可以借助编程方式通过 Attributes 集合设置 TextBox Web 控件的 onfocus 和 onblur 客户端属性,如下所示:

TextBoxControl.Attributes("onfocus") = "this.style.backgroundColor='yellow';"
TextBoxControl.Attributes("onblur") = "this.style.backgroundColor='white';"

将客户端代码与客户端事件结合在一起的这种技术通常用于提供丰富的、交互式的用户体验。稍后,我们将会在本文中说明如何使用这种技术来显示基于用户操作的确认对话框。

返回页首
实现常用客户端功能
在我们研究 ASP.NET 方法(涉及动态地将客户端脚本添加到网页)后,让我们将注意力转移到这个知识。本文的其余部分重点讲述常用客户端任务,例如显示警告框、确认框、弹出窗口等等。具体说来,我们将创建包含一组方法的类,这组方法可用在 ASP.NET 项目中,从而快速、便捷地提供这样的功能。

我们将要分析的、贯穿本文剩余部分的 Visual Basic .NET 代码可以在本文的代码下载中获得。

显示警告框
一个常用的客户端要求就是显示警告框。警告框是一个客户端模式对话框,通常用于为最终用户提供某些重要的信息。警告框的示例如图 1 所示。警告框是通过客户端 JavaScript alert 函数来进行显示的,该函数接受一个单个的参数,即要显示的消息。显示警告框相当简单和直接;实际上,本文前面已经显示了一个示例。

为了使页面开发人员尽可能简单的显示警告框,让我们创建一个名为 ClientSidePage 的类,其中包含一个名为 DisplayAlert(message) 的方法。这个类将会继承 Page 类。想要利用这些客户端 helper 方法的页面开发人员需要使他们的代码隐藏类继承这个 ClientSidePage 类,而不是继承默认的 Page 类。以下代码显示了带有其第一个方法 DisplayAlert 的 ClientSidePage 类。

Public Class ClientSidePage
Inherits System.Web.UI.Page

Public Sub DisplayAlert(ByVal message As String)
RegisterClientScriptBlock(Guid.NewGuid().ToString(), _
"script language=""JavaScript"">" & GetAlertScript(message) & "/script>")
End Sub

Public Function GetAlertScript(ByVal message As String) As String
Return "alert('" & message.Replace("'", "\'") & "');"
End Function

End Class

请注意,这个类派生自 System.Web.UI.Page 类。DisplayAlert 方法只是使用 RegisterClientScriptBlock 方法,在警告框中显示提供的 message。由于这个方法可能会由单个页面多次调用,每个调用将使用其密钥的 GUID(是“全局唯一标识符”的首字母缩写)。传递到 alert 函数的字符串会使用撇号分隔,message 中的任意撇号都必须进行转义(JavaScript 将撇号转义为 \')。

要将这段代码用于 ASP.NET Web 应用程序中,您需要将新类添加到 ASP.NET 应用程序中。在 Visual Studio .NET 中,右键单击解决方案资源管理器中的 ASP.NET Web 应用程序项目名,然后选择添加新类。然后,剪切上述代码并将其粘贴到该类中。接下来,在要利用该代码的 ASP.NET 网页中,您需要修改代码隐藏类,以便它从 ClientSidePage 类而不是从 Page 中继承。以下代码显示了派生自 ClientSidePage 并使用 DisplayAlert 方法的示例代码隐藏类。

Public Class WebForm1
Inherits ClientSidePage

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
DisplayAlert("Hello, World!")
End Sub

...
End Class

请注意,ClientSidePage 类不仅具有可以生成完整客户端 script> 元素的 DisplayAlert 方法,还具有可以返回客户端脚本无 script> 标记的 GetAlertScript 方法。第二个方法可用于您希望基于某个客户端事件显示警告的情况中。例如,如果您希望在特定文本框接收到焦点的任意时间显示警告,可以将以下代码添加到服务器端代码隐藏类中:

TextBoxControlID.Attributes("onfocus") = GetAlertScript(message)

将焦点设置为页面加载时的窗体字段
您是否注意到当访问 Google 时,焦点自动设置在搜索文本框呢?这一点小的“功能”使得搜索 Google 更快 - 在访问 Google 时您不必再花费时间移动鼠标,然后单击文本框。更确切的说,您只需在页面加载时键入即可。将焦点设置到窗体字段(可以是文本框、单选按钮、复选框或下拉列表)只要求几行客户端 JavaScript 代码。让我们将方法添加到 ClientSidePage 类,该类将在页面加载时自动向指定的 Web 控件中添加焦点。这种方法需要发出如下所示的客户端脚本:

script language="JavaScript">
function CSP_focus(id) {
var o = document.getElementById(id);
if (o != null)
o.focus();
}
/script>

... Form fields ...

... Form fields ...

script language="JavaScript">
CSP_focus(id of element to focus);
/script>

客户端函数 CSP_focus 接受字符串参数,窗体字段的 ID 被设置为焦点,并且从 DOM 中检索 HTML 元素。然后,调用检索元素的 focus() 函数。在网页的底部,在指定所有窗体字段后,我们需要调用 CSP_focus 方法,该方法在想要设置焦点的窗体字段的 ID 中传递。

下面的方法 GiveFocus(Control) 使用 RegisterClientScriptBlock 和 RegisterStartupScript 方法来生成所需要的客户端脚本。

Public Sub GiveFocus(ByVal c As Control)
RegisterClientScriptBlock("CSP-focus-function", _
"script language=""JavaScript"">" & vbCrLf & _
"function CSP_focus(id) {" & _
" var o = document.getElementById(id); " & _
"if (o != null) o.focus(); " & _
"}" & vbCrLf & _
"/script>")

RegisterStartupScript("CSP-focus", _
"script language=""JavaScript"">CSP_focus('" & _
c.ClientID & "');/script>")
End Sub

要从其代码隐藏类继承 ClientSidePage 的 ASP.NET 网页中使用 GiveFocus 方法,可以简单地在 Page_Load 事件处理程序中调用 GiveFocus,并传递应该在页面加载时设置其焦点的 Web 控件。例如,要将焦点设置为 TextBox Web 控件 TextBoxControl,请使用以下 Page_Load 事件处理程序:

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
GiveFocus(TextBoxControl)
End Sub

打开弹出窗口
尽管弹出窗口作为广告发布者的工具在 Internet 上早已臭名昭著,但是很多 Web 应用程序还是因为使用弹出窗口而得到了好处。例如,您想要某个页面在 DataGrid 中显示数据库项目的列表,同时带有可以编辑每个特定项目的链接。不再使用 DataGrid 的内联编辑功能,您可能希望在用户选择编辑 DataGrid 时打开弹出窗口,其中弹出窗口包含带有可编辑 DataGrid 字段的文本框列表。(您希望这么做的一个原因在于可能存在大量的可编辑字段,但是您只想显示 DataGrid 中最适当的字段,因此要消除使用 DataGrid 的内置编辑功能的可能性。)

要显示弹出窗口,请使用 JavaScript 函数 window.open(),它使用很多可选输入参数,其中三个密切相关的参数是:

• 加载弹出窗口的 URL。

• 弹出窗口的字符串名称。

• 弹出窗口的特性,例如高度和宽度、窗口是否可以调整大小等等。


window.open() 函数的完整讨论已经超出了本文的范围;要学习更多内容,请参阅技术文档。

与其他显示警告框的方法相似,ClientSidePage 类包含用于显示弹出窗口的两个方法 - 一个呈现显示窗口的自包含 script> 块,另一个仅返回 JavaScript 脚本本身。除了打开弹出窗口的方法外,还有一组关闭当前窗口的方法。(可能会出现这样的情况,您希望以编程方式基于某些客户端或服务器端事件来关闭弹出窗口。)

Public Sub DisplayPopup(ByVal url As String, ByVal options As String)
RegisterStartupScript(Guid.NewGuid().ToString(), _
"script language=""JavaScript"">" & _
GetPopupScript(url, options) & _
"/script>")
End Sub

Public Function GetPopupScript(ByVal url As String, _
ByVal options As String) As String
Return "var w = window.open(""" & _
url & """, null, """ & options & """);"
End Function

Public Sub CloseWindow(Optional ByVal refreshParent As Boolean = False)
RegisterClientScriptBlock("CSP-close-popup", _
"script language=""JavaScript"">" & _
GetCloseWindowScript(refreshParent) & "/script>")
End Sub

Public Function GetCloseWindowScript(Optional _
ByVal refreshParent As Boolean = False) As String
Dim script As String
If refreshParent Then
script &= "window.opener.location.reload();"
End If

Return "self.close();"
End Function

该代码的执行示例可以在本文的代码下载中找到。同时,您还将发现一个示例网页,它具有一个在与 ASP.NET 网页相同的目录中列出文件的 DataGrid。这个 DataGrid 具有两列:显示超级链接的 TemplateColumn,当单击时打开显示所选文件内容的弹出窗口;以及该文件的名称(如图 3 所示)。



图 3. 带有弹出窗口的 DataGrid

DataGrid 的标记利用 GetPopupScript 方法,如下所示:






View File



HeaderText="Filename">



ASP.NET 网页 ViewFile.aspx 打开其名称在 querystring 中指定的文件并显示其内容(如图 4 所示)。



图 4. 在弹出窗口中显示 Web.config 的内容

注 弹出窗口最适用于只有 Intranet 应用程序的情况,因为很多 Internet 用户利用某种弹出阻止软件,例如 Google 工具栏。实际上,利用 Microsoft Windows XP Service Pack 2,Microsoft Internet Explorer 将会在默认情况下配置为阻止弹出窗口。但是,当用户访问受信任站点或本地 Intranet 区域中的站点时,弹出窗口将仍然会出现。有关在 Windows XP Service Pack 2 中阻止 Internet Explorer 弹出窗口功能的详细信息,请务必阅读 Changes to Functionality in Microsoft Windows XP Service Pack 2。

在回发前确认
在本文的前面部分,我们研究了如何显示客户端警告框,这是带有“OK”按钮的模式对话框。JavaScript 提供被称为确认对话框的更具有交互风格的警告框。使用 confirm(message) 函数显示确认对话框并通过 message 输入参数与“OK”和“Cancel”按钮指定显示带有文本的对话框的效果。如果用户单击“OK”,confirm(message) 函数会返回 true;如果他们单击“Cancel”,则返回 false。

通常情况下,确认对话框用于确保用户在提交窗体之前希望继续。当单击 HTML 元素(例如提交按钮)提交窗体时,如果 HTML 元素触发返回 false 的客户端事件处理程序,窗体提交就被取消。通常,确认对话框用于网页中,如下所示:

form ...>
onclick="return confirm('Are you sure you want
to submit this form?');" />
/form>

当用户单击“Click Me to Submit the Form”按钮时,他们将会看到确认对话框,询问他们是否确实希望提交该窗体(如图 5 所示)。如果用户单击“OK”,confirm() 将返回 true,该窗体将被提交。但是,如果他们单击“Cancel”按钮,confirm() 将返回 false,该窗体提交将被取消。



图 5. 确认 JavaScript 的结果

设想您具有一个 DataGrid带有标签为“Delete”的一列按钮。在单击该按钮时,该窗体将会回发,并且选定的记录将被删除。在此例中,您可能希望复查用户是否确实希望删除该记录。此时,要使用客户端确认对话框将会非常理想。您可以用对话框提示用户,声明如下所示的内容:“This will permanently delete the record.Are you sure you want to continue?”如果用户单击“OK”,该窗体将会回发,并且记录将被删除;如果他们单击“Cancel”,该窗体将不会回发,而且记录也不会被删除。

要添加在单击按钮后显示确认对话框所必需的客户端 JavaScript,请简单地使用 Attributes 集合来添加客户端 onclick 事件处理程序。具体说来,要将 onclick 事件处理程序代码设置为:return confirm(message);。为了提供 DataGrid 的 ButtonColumn 的这种功能,您需要在 DataGrid 的 ItemCreated 或 ItemDataBound 事件处理程序中以编程方式引用 Button 或 LinkButton 控件,并且在那里设置 onclick 属性。有关详细信息,请参阅 http://aspnet.4guysfromrolla.com/articles/090402-1.aspx。

确认 AutoPostBack DropDownLists
尽管通常在单击按钮时使用确认对话框,但是还可以在更改下拉列表时使用它们。例如,您可能具有一个当特定的 DropDownList Web 控件发生更改时会自动回发的网页。(DropDownList Web 控件具有一个 AutoPostBack 属性,如果设置为 True,只要 DropDownList 的选定项目发生更改就会导致窗体回发。)

直观地讲,您可能认为对 DropDownList 添加确认对话框与对 Button Web 控件添加这种对话框相同。也就是说,简单地将 DropDownList 的客户端 onchange 属性更改为如下内容:return confirm(...);。使用:

DropDownListID.Attributes("onchange") = "return confirm(...);"

遗憾的是,这并不会按期望工作,因为 AutoPostBackDropDownList 的 onchange 属性将设置为会导致回发的 JavaScript,即对客户端 __doPostBack 函数的调用。当您自己借助编程方式设置 onchange 属性时,最后的结果是呈现的客户端 onchange 事件处理程序同时具有您的代码和对 __doPostBack 的调用:



记住,我们确实希望发生的情况是,如果确认返回 true,就调用 __doPostBack 函数,因为之后页面将会被回发。通过利用 Attributes 集合将 onchange 事件设置为:if (confirm(...)),我们可以完成这一操作,而该代码会生成以下标记,该标记正是我们所希望的:



乍看起来,这似乎会具有所期望的效果。如果用户从下拉列表中选择不同的项目,将会出现一个确认框。如果用户单击“OK”,该窗体将回发;如果用户单击“Cancel”,该窗体回发会暂停。尽管问题在于下拉列表维持用户选定的项目以启动下拉列表的 onchange 事件。例如,设想下拉列表加载正在进行选择的项目 x,然后用户选择项目 y。这将会触发下拉列表客户端 onchange 事件,它将会显示确认对话框。现在,设想用户点击“Cancel”- 下拉列表将仍然选择项目 y。我们希望的是将选择转回到项目 x。

要实现此目的,我们需要做两件事情:

1.
编写一个“记住”选定下拉列表项目的 JavaScript 函数。

2.
在下拉列表的客户端 onchange 事件中,如果用户单击“Cancel”,您需要将下拉列表转换回“已记住的”值。


步骤 1 必须为下拉列表和函数(当页面加载时运行,并且记录下拉列表的值)创建全局脚本变量。步骤 2 要求为下拉列表的客户端 onchange 属性更改为如下所示内容:

if (!confirm(...)) resetDDLIndex(); else __doPostBack();

其中 resetDDLIndex 是 JavaScript 函数,它将下拉列表选定的值返回到“已记住的”值。用于此目的的客户端脚本应该如下所示:



script language="JavaScript">
var savedDDLID = document.getElementById("ddlID").value;

function resetDDLIndex() {
document.getElementById("ddlID").value = savedDDLID;
}
/script>

通过在 ClientSidePage 类中创建 helper 方法,这个必要的脚本可以轻松地生成。

Public Sub ConfirmOnChange(ByVal ddl As DropDownList, ByVal message As String)
'Register the script block
If Not IsStartupScriptRegistered("CSP-ddl-onchange") Then
RegisterStartupScript("CSP-ddl-onchange", _
"script language=""JavaScript"">" & _
"var CSP_savedDDLID = " & _
document.getElementById('" & _
ddl.ClientID & "').value;" & vbCrLf & _
"function resetDDLIndex() {" & vbCrLf & _
" document.getElementById('" & " & _
" ddl.ClientID & "').value = CSP_savedDDLID;" &
vbCrLf & _
"}" & vbCrLf & _
"/script>")
End If

ddl.Attributes("onchange") = _
"if (!confirm('" & message.Replace("'", "\'") & _
"')) resetDDLIndex(); else "
End Sub

要使用这段代码,简单地调用网页上每个 AutoPostBackDropDownList 的该方法,当网页上的选定项目发生更改时要在该网页上显示对话框。

未保存而退出时进行确认
在我所创建的大多数每个数据驱动的 Web 应用程序中,始终会有用户可以编辑数据库特定信息的特定页面。非常简单的一个示例是带有一系列 TextBox 和 DropDownList Web 控件的页面,而数据库数据填充在这些控件中。用户可以进行任何适当的修改,然后单击“Save”按钮将他们所做的更改保存到数据库。

当我创建这些页面时,通常会以两个 Button Web 控件来结束页面:“Save”按钮和“Cancel”按钮。“Save”按钮将任意更改保存回数据库,而“Cancel”按钮不保存任何更改退出页面。尽管两个按钮看起来可能是一个完美的设计,但有时用户会在他们想要单击“Save”按钮时意外地单击“Cancel”按钮,这样就会丢失了他们对数据所做的所有更改。为防止这种情况发生,可以在“Cancel”按钮上使用确认框,它只有在网页上的任意文本框或下拉列表发生更改时才会出现。也就是说,如果用户对数据进行了任意更改,然后单击“Cancel”,确认框将会提示他们是否确实要在不保存的情况下退出。(如果用户只是单击“Cancel”,而没有更改任何数据,将不会显示这样的确认框。)

这种用户体验可以通过少量客户端 JavaScript 来实现。基本上可以说,它需要一个 JavaScript 全局变量 isDirty,在初始时为 false,但只要触发窗体字段的 onchange 事件,它就会设置为 true。如果 isDirty 为 true,则还有一个显示确认对话框的 JavaScript 函数。“Cancel”按钮的 onclick 客户端事件处理程序限定为从该 JavaScript 函数返回结果。以下 HTML 说明了这个概念:

script language="JavaScript">
var isDirty= false;
function checkForChange(msg) {
if (isDirty) return confirm(msg); else return true;
}
/script>

Name:


id="btnCancel"
onclick="return checkForChange('You have made changes to the data
since last saving. If you continue, you will lose these
changes.');" />

可以通过将该脚本生成移动到 ClientSidePage 类,简单地生成这个脚本。具体说来,我们可以创建下列三个方法:

Protected Sub RegisterOnchangeScript()
If Not IsClientScriptBlockRegistered("CSP-onchange-function") Then
RegisterClientScriptBlock("CSP-onchange-function", _
"script language=""JavaScript"">" & _
"var isDirty= false;" & vbCrLf & _
"function CSP_checkForChange(msg) {" & vbCrLf & _
" if (isDirty) return confirm(msg); " & _
"else return true;" & vbCrLf & _
"}" & vbCrLf & _
"/script>")
End If
End Sub

Public Sub MonitorChanges(ByVal c As WebControl)
RegisterOnchangeScript()
If TypeOf c Is CheckBox Or TypeOf c Is CheckBoxList _
Or TypeOf c Is RadioButtonList Then
c.Attributes("onclick") = "isDirty = true;"
Else
c.Attributes("onchange") = "isDirty = true;"
End If
End Sub

Public Sub ConfirmOnExit(ByVal c As WebControl, ByVal message As String)
RegisterOnchangeScript()
c.Attributes("onclick") = _
"return CSP_checkForChange('" & message.Replace("'", "\'") &
"');"
End Sub

要创建表现这个行为的网页,我们只需要将其服务器端代码隐藏类派生自 ClientSidePage,并且在 Page_Load 事件处理程序中,为需要客户端 onchange 事件的每个 Web 控件对 MonitorChanges 进行调用,并且为在单击时应该显示警告用户是否进行更改并退出页面的每个 Button、LinkButton 和 ImageButton,对 ConfirmOnExit 进行调用。

注MonitorChanges 方法使用 onclick 客户端事件,而不是用于 CheckBox、CheckBoxList 和 RadioButtonList Web 控件的 onchange。这是因为这些控件将 标记或 table> 限制在复选框或很多复选框或单选按钮附近。在我利用 Internet Explorer 进行测试时,我发现在应用到 或 table> 时,选中复选框或单击单选按钮并没有选择 onchange 事件,但是却触发了 onclick 事件。

图 6 显示了带有两个 TextBox Web 控件、一个 DropDownList Web 控件和一个 CheckBox Web 控件的 ASP.NET 网页示例。如下面的 Page_Load 事件处理程序所示,所有这些 Web 控件都会被监视所做的更改。配置“Cancel”按钮 btnCancel,这样如果在进行更改后单击它,将显示一个确认对话框。



图 6. 带有确认的对话框

Public Class ConfirmOnExit
Inherits ClientSidePage

Private Sub Page_Load(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles MyBase.Load
'Specify what controls to check for changes
MonitorChanges(name)
MonitorChanges(age)
MonitorChanges(favColor)
MonitorChanges(chkSilly)

ConfirmOnExit(btnCancel, _
"You have made changes to the data since last saving." & _
" If you continue, you will lose these changes.")
End Sub

...
End Class

注客户端 onchange 事件不能用于 Netscape 的旧版本中。同样,Internet Explorer 5.0 的 onchange 事件也具有一些已报告的问题(在 Internet Explorer 5.01 SP 1 中已经修复)。

而且,这种方法将不会像利用 DropDownList Web 控件那样将 AutoPostBack 设置为 True,而是回发将重置 isDirty 的值。这个问题有很多解决方案,例如使用隐藏窗体字段,指示出回发窗体数据是否以 dirty 开始。我将实现这个操作的过程作为练习留给读者。

创建客户端 MessageBox 控件
由于确认对话框是一种防止意外点击的非常好的方法,可以潜在地降低到 Web 服务器的回发数量,有几种特定方案您可能希望显示确认对话框,并且能够在服务器端确定用户是否单击了“OK”或“Cancel”。(记住,对于确认对话框,如果用户单击“Cancel”,该窗体则不会回发。)而且,JavaScript 中的警告和确认框在外观上非常受限。幸运的是,客户端 VBScript 通过其 MsgBox 函数提供了更丰富的消息框体验。

在过去的项目中,我需要客户端模式消息框,无论单击什么按钮,它都可以引起回发。作为响应,我构建了自定义编译的 ASP.NET 服务器控件来满足这些要求。此外,客户端消息框还使用 VBScript 的 MsgBox 函数来提供更丰富的消息框体验。

注 在 Microsoft Internet Explorer 浏览器中,VBScript 只作为客户端脚本编辑语言。要考虑到这一点,如果访问浏览器是 Internet Explorer,那么我的服务器控件只能使用 VBScript。如果是非 Internet Explorer 浏览器,则服务器端控件使用 JavaScript。

对这种自定义服务器控件深入的讨论可以完全保证整个文章的正确性,因此无需将重点放在控件的内部工作原理上,让我们分析如何在 ASP.NET 网页中使用 MessageBox 控件。(控件的完整资源以及使用该控件的示例 ASP.NET 网页都可以从本文的下载中获得。)

要在 ASP.NET 网页中使用 MessageBox 控件,首先要将 MessageBox 控件添加到 Visual Studio .NET 工具箱。通过右键单击工具箱、从工具箱中选择“Add/Remove Items”、然后浏览到 MessageBox 程序集可以实现上述任务。要将客户端消息框添加到网页,只要将其从工具箱拖动到该设计器即可。图 7 显示了 Visual Studio .NET 设计器中的 MessageBox 控件。



图 7. 显示模式消息框

MessageBox 类具有很多可以进行配置的属性,以调整消息框的外观:

• Buttons. 指定要显示的按钮。ButtonOptions 枚举中定义的选项可以为:OkOnly、OkCancel、AbortRetryIgnore、YesNoCancel、YesNo 或 RetryCancel。

• DisplayWhenButtonClicked. 点击后将显示客户端消息框的按钮 Web 控件的 ID。如果希望由于点击某个特定按钮而显示消息框,请使用这个属性。

• Icon. 显示在消息框中的图标;选项定义于 IconOptions 枚举中。有效的值为:Critical、Question、Exclamation 和 Information。

• Prompt. 显示在消息框中的文本。

• Title. 消息框的标题。


一旦将 MessageBox 控件添加到 ASP.NET 网页,下一个挑战就是使其根据特定客户端操作进行显示。DisplayWhenButtonClicked 属性允许您指定页面上按钮 Web 控件的 ID点击后将会显示消息框。另外,您还可以通过调用客户端函数 mb_show(id) 来显示消息框,其中 ID 是 MessageBox 控件的 ID。

不管您选择在消息框中显示什么按钮配置,当单击任意按钮时,随后就会发生回发并且触发 MessageBox 控件的 Click 事件。通过在设计器中简单地双击 MessageBox,可以为此事件创建一个事件处理程序。事件处理程序的第二个输入参数是类型 MessageBoxClickedEventArgs,它包含一个返回用户单击消息框按钮的信息的 ButtonClicked 属性。

MessageBox 控件在如下情况下非常有用,当您希望快速为用户提供模式对话框,而不管用户的选择以及回发中的结果。要查看操作中的 MessageBox 控件,请签出源代码下载中的 MsgBoxDemo.aspx 页面。

返回页首
小结
本文一开始就研究了网页中客户端脚本的常见使用,然后转向分析将客户端脚本插入到 ASP.NET 网页中的方法和技术。正如我们看到的那样,Page 类包含很多旨在借助编程方式从服务器端代码隐藏类插入客户端脚本块的方法。这些方法也常用于自定义编译的服务器控件,请参阅我以前文章中的讨论: Injecting Client-Side Script from an ASP.NET Server Control.

除了添加脚本块外,客户端功能通常必须与由某些 HTML 元素引发的客户端事件结合在一起。要借助编程方式通过服务器端代码隐藏类指定 Web 控件的客户端事件处理程序,请使用 Attributes 集合,它可用作所有 Web 控件的属性。

本文的后半部分应用了前半部分中涉及的内容,显示了如何在 ASP.NET 网页中实现常用客户端功能。我们看到了如何扩展 Page 类,这样我们可以利用代码隐藏类轻松地显示警告框、将页面加载上的焦点设置到特定 Web 控件、如何显示弹出窗口以及如何显示确认对话框。我们还研究了创建自定义服务器控件,它使用 VBScript 来提供更丰富的客户端消息框用户体验,而且无需考虑单击按钮就可以导致回发。

祝大家编程愉快!

2.
How to use clientscript in custom controls
By Andy Smith
From http://weblogs.asp.net/asmith/articles/25465.aspx

So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.

So you want to build a custom asp.net control. And you want to have some whiz bang, complex script that goes with it. Well, there are a few methods on System.Web.UI.Page that help you emit script into the right places, but the docs on these methods don't really tell you the whole picture at once. A beginning control developer ends up hacking his way thru things, and asking tons of questions on this stuff in the lists. It's taken me a while, but now I've got what I consider to be the Right Way© to emit script for your controls, and I will now impart this knowledge onto the masses ( err, the 7 people who read this blog ). Not to say that I follow this advice to the letter for every control of mine, but hey, it was a learning experience.

First off, here are the relevant methods on Page for script stuff:

GetPostBackEventReference
RegisterArrayDeclaration
RegisterClientScriptBlock
RegisterStartupScript
There are more, for more esoteric needs, but this is what i'm covering right now
GetPostBackEventReference
Like the docs hint at but don't quite say, this method returns the script that calls __doPostBack. However, never type the function name "__doPostBack". Neither function name nor the specifics of the args it takes, are guarenteed to be the same for future versions of asp.net. Just use the returned value of this function instead. Always.

The Three Registers
Here's the deal on how these three should work together. RegisterClientScriptBlock says what to do, RegisterArrayDeclaration says who to do it to, and RegisterStartupScript just says Go.

If that was confusing, here's a lot more detail...

RegisterClientScriptBlock is where you put generic, non-control-instance-referencing code. That's not to say it can't have knowledge of the control design, but it shouldn't have knowledge of any specific controls on the page, by ID or any other way. Any of your control's IDs should be variables that this library uses. This lets you have 1 big library of code that can just be plopped onto the page with no worries about multiple controls or page structure or anything. It should have an entry point function ( I recommend something like MetaBuilders_WebControls_FooControl_Init ) where everything gets set up, properties are set, event handlers are added, etc. I'll go into good practices for this script later, for now i'm going to stay at the overview level.

RegisterArrayDeclaration is where you put the control id's that you are avoiding in the code library. Simply pick a name for your array ( I recommend the namespace qualified plural name of your control, with _'s, MetaBuilders_WebControls_FooControls ) and use the ClientID or UniqueID as the value, depending on needs. ( needs which I'll go over later ) This array is then accessed in your library's Init function in order to get references to each control instance on the page. Sometimes you'll find a need to include more than just the ID in the array value, when that happens, you can use a special property-value syntax i'll get into later.

RegisterStartupScript should generally have one line of code. Just call the Init function and kick things off.

How To Write The Library
Ok, so far I've only said that the lib should have an Init, and use the ClientIDs in the registered array to do stuff. Here's how I would write the lib... First, a typical Init skeleton looks something like this:

function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }

// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var myFooControl = document.getElementById( MetaBuilders_FooControls[i] );

//TODO Do stuff with myFooControl

}
}

function MetaBuilders_FooControl_BrowserCapable() {
if ( typeof( document.getElementById ) == "undefined" ) {
return false;
}
//TODO Add any more checks you need to
return true;
}

If you need to support browsers that don't have getElementById, then you'll need to do a few emit the UniqueID into the array, and use a form/input searching function to find the correct control. That would look something like this:

function MetaBuilders_FooControl_FindControl(uniqueID) {
for( var i = 0; i < document.forms.length; i++ ) {
var theForm = document.forms[i];
var theControl = theForm[uniqueID];
if ( theControl != null ) {
return theControl;
}
}
return null;
}

But in short, the Init method loops thru the array of IDs, and does fun stuff with the control. But what if you have composite control, and you want to do fun stuff with the child controls. Then your array declaration and your Init function will look a bit different. You'll want to give RegisterArrayDeclaration the ClientIDs of all the child controls you want to interact with instead of just the one parent. So lets say you have a composite control with two textboxes inside. your array bit might look like this:

Page.RegisterArrayDeclaration("{ ID:'" + this.ClientID + "', firstTextBoxID:'" + this.firstTextBox.ClientID + "', secondTextBoxID:'" + this.secondTextBox.ClientID + "' }");

then your init function might change to this:

function MetaBuilders_FooControl_Init() {
// Make sure the browser supports the DOM calls or JScript version being used.
if ( !MetaBuilders_FooControl_BrowserCapable() ) { return; }

// Loop thru the array of control ClientIDs and get a reference to the element
for ( var i = 0; i < MetaBuilders_FooControls.length; i++ ) {
var fooControlProperties = MetaBuilders_FooControls[i];
var myFooControl = document.getElementById( fooControlProperties.ID );
var myFirstTextBox = document.getElementById( fooControlProperties.firstTextBoxID );
var mySecondTextBox = document.getElementById( fooControlProperties.secondTextBoxID );

//TODO Do stuff with myFooControl and the child controls

}
}

The syntax I used in the array declaration ends up looking like this:

var MetaBuilders_FooControls = new Array( { ID:'fooControl1', firstTextBoxID:'fooControl1_firstTextBox', secondTextBoxID:'fooControl1_secondTextBox } );

It declares each item in the array as an object with the properties you set with the propertyName:value syntax. This style can be used with any properties, not just child control ids.

This brings us to custom server property values that the script needs to use. If you have a property on your control that simply needs to be sent to the script for use there, then the easiest way is to add another item to the RegisterArrayDeclaration call and change Init to grab it from the array. If the property needs to be a two-way variable, get/set on both server and client, then I suggest you add a HtmlInputHidden control and use its .Value. The serverside property will simply wrap its value, and you can get-set it on the clientside easily once you emit its ClientID in the array.

Ok... now... clientside events. I suggest that you attach event handlers in the Init function instead of setting the onFoo attributes on the control. The reason for this is that it keeps all your script in one place. The serverside c# or VB code doesn't need to know the specifics of your javascript, it only needs to give the script the info it needs for the current instance via the array declaration. This help speed up dev time, as you can stay in one file for changing event handlers and such.

Ok, so what's the code look like then? Well, you basically have 2 choices, you can either set the .onfoo property, or use the addEventListener/attachEvent methods. Using the onfoo property method allows you to easily access the calling control of the event via the "this" keyword, which comes in quite handy. However, it has the problem that it completely takes over the event. If there's also a tag attribute for it, this will replace that one. The other method, of course, has the exact opposite characteristics. "this" can't refer to the control, but it doesn't interfere with anybody else.

So how do you decide which is best? My recommendation is to use the .onfoo property if you are attaching events to your own child controls, and use the addEventListener/attachEvent methods if you are attaching events to controls outside of your jurisdiction. So lets look at some code...

This example will show the .onfoo property way. this code goes inside the Init function:

myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;

mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;

....

function MetaBuilders_FooControl_UpdateOtherTextBox() {
// note that by using the method, "this" refers to the textbox for the event.
this.OtherTextBox.value = this.value;
}

Now, whenver one textbox changes, the other will be updated. Take a closer look at the event hookup. First I create a new method on each textbox, UpdateOtherTextBox, by setting this new property to a function pointer. then I set the onchange event to the textbox's own method. This two-line technique is what allows me to use "this" in the handler.

However, when you are attaching to random controls, you don't want to be just overtaking their events like this, so you have to do some gymnastics. Here's an example from my DefaultButtons control where I attach to the onfocus and onblur events of page-level textboxes:

if ( typeof( inputControl.addEventListener ) != "undefined" ) {
inputControl.addEventListener("focus",DefaultButton_RegisterDefault,false);
inputControl.addEventListener("blur",DefaultButton_UnRegisterDefault,false);
} else if ( typeof ( inputControl.attachEvent ) != "undefined" ) {
inputControl.attachEvent("onfocus",DefaultButton_RegisterDefault);
inputControl.attachEvent("onblur",DefaultButton_UnRegisterDefault);
} else {
inputControl.onfocus = DefaultButton_RegisterDefault;
inputControl.onblur = DefaultButton_UnRegisterDefault;
}


The reason that this code is so big is because browsers are very different. addEventListener is the official W3C way of attaching events. attachEvent is the IE way, and I default to the .onfoo way if neither is supported. The problem is that this complicates my handler code, as "this" no longer refers to the control raising the event. To fix this, you need to get a handler on the data for the event. Here's how you get the element raised by an event, again from my DefaultButtons code:

function DefaultButton_RegisterDefault(e) {
// src here is the control which raised the event.
var src = DefaultButton_GetSrcElement(e);

//Usefull stuff removed for clarity
}

function DefaultButton_GetSrcElement(e) {
if ( typeof( window.event ) != "undefined" ) {
return window.event.srcElement;
}
if ( e != null && typeof( e.target ) != "undefined" ) {
return e.target;
}
return null;
}

Ok, so once you have your custom properties and your event handlers set up, now you just implement whatever cool stuff you want to make your control do its thing.

Oh, one more thing with the code. If you take another look at this code:

myFirstTextBox.OtherTextBox = mySecondTextBox;
myFirstTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
myFirstTextBox.onchange = myFirstTextBox.UpdateOtherTextBox;

mySecondTextBox.OtherTextBox = myFirstTextBox;
mySecondTextBox.UpdateOtherTextBox = MetaBuilders_FooControl_UpdateOtherTextBox;
mySecondTextBox.onchange = mySecondTextBox.UpdateOtherTextBox;

You'll see that I actually make properties on each child control that reference the other child controls. This is a very useful thing to do, as it makes referencing the related child controls from the event handlers much easier.

and now we move on to...

Packaging
UPDATE: This part has changed, thanks to some great comments on the article.
So now that you know who to write the code, how to you incorporate it into your projects? I use vstudio for all my control development, so I'll tell you how I do it with that tool. What I've found to be the best is to simply put all the script code into a .js file as an embedded resource. Simply right-click on the project, and choose Add New Item, choose script file, and set it as an embedded resource in the properties window.

Now to emit your code to the browser you use code like this:

if ( !Page.IsClientScriptBlockRegistered(scriptKey) ) {
using (System.IO.StreamReader reader = new System.IO.StreamReader(typeof(FooControl).Assembly.GetManifestResourceStream(typeof(FooControl), "FooControl.js"))) {
String script = "script language='javascript' type='text/javascript' >\r\n\r\n /script>";
this.Page.RegisterClientScriptBlock(scriptKey, script);
}
}


Ok, that code is nice, but where do you put it?
This is generally how I handle the actual task of script registration:

protected override void OnPreRender(EventArgs e) {
base.OnPreRender(e);
this.RegisterScript();
}

protected virtual void RegisterScript() {
// All the code that calls Page.RegisterFoo methods goes here.
}

PreRender is generally a good place to put script registrations because it is the last event where you can call these methods and they still take effect. You want to do it as late as possible because you never know when a page developer is going to change properties on your control that might change the script you produce.

Another tip for the serverside code... You generally don't want to Register your script if your .Visible property is false. And, depending on your script and functionality, you might not want to Register your script if your .Enabled property is false.

So anyway, I hope you got some useful information here. If you have any questions, feel free to send me a message or leave a comment.

P.S. almost none of the code here has actually been tested. I just wrote this stuff directly into the blog. I don't think there are problems with the code itself, but hey, I may have missed something, and this was intended as conceptual code anyway. Let me know if you find something obviously wrong.

Friday, October 15, 2004

 

Some notes on file download

1.
作者:gdsean
出处:http://www.csdn.net/develop/read_article.asp?id=15653

页面重定向一般有两个页面,第一个页面是引导页,利用meta标签的HTTP-EQUIV=REFRESH重新获得新的页面地址。所以可以用一个页面标签解释的办法来取得新的页面地址。

我们采用一个正则表达式:

String express = "HTTP-EQUIV\\s*=\\s*\"*REFRESH\"*([^>]*)URL\\s*=\\s*([^>]*)>";

来匹配引导页。如果匹配到有定向标签的就取得标签中的url,处理相对地址变为绝对地址,然后再下载这个地址的页面,这个过程可以迭代,直到没有匹配到定向标签,最后下载到的页面为最终页面。

Java例子代码:

[code]
private boolean tryRedirect(StringBuffer sb){

String text = sb.toString();//取得页面正文

if(matcher.contains(text,pattern)){//如果匹配表达式成功

MatchResult result = matcher.getMatch();

String url = result.group(2);//取得标签里面的新地址

url = url.trim();

if(url.charAt(url.length()-1)=='\'){

url = url.substring(0,url.length()-1);

}

if(url.charAt(url.length()-1)=='\"'){

url = url.substring(0,url.length()-1);

}

//转换相对地址为绝对地址

currentURL=com.westwind.infogate.util.URLProcess.RelativelyToFull(url,currentURL);

if(currentURL==null)

return false;

currentURL = URLProcess.addHttphead(currentURL);

return true;

}

return false;

}
[/code]

(按:有些东西,不知道就是一点也不明白。而其实原理很简单。用到的时候再学吧!)

2.
C#下的类似方法
作者:纯朴的狗熊
出处:http://blog.joycode.com/liuhuimiao/posts/18180.aspx

1) 首先创建一个类库项目ClassLibrary
[code]
using System;
using System.Web; // 引用System.Web组件
namespace ClassLibrary1
{
public class MyHandler : IHttpHandler
{
public MyHandler()
{
}
#region IHttpHandler 成员
public void ProcessRequest(HttpContext context)
{
// 跳转到WebForm1.aspx,由WebForm1.aspx输出rar文件
HttpResponse response = context.Response;
response.Redirect("http://193.100.100.56/TestWebSolution/WebApplication1/WebForm1.aspx");
}

public bool IsReusable
{
get
{
// TODO: 添加 MyHandler.IsReusable getter 实现
return true;
}
}
#endregion
}
}
[/code]

2) 创建测试用的Web项目WebApplication。在配置文件Web.config文件节点里增加如下节点:



httpHandlers>

3) 在WebForm.aspx里增加一个文本为“下载”的Button,其Click事件如下(点这里查看):

[code]
FileInfo file = new System.IO.FileInfo(@"G:\WebCenter\TestWebSolution\WebApplication1\test.rar");

// FileInfo 类在 System.IO 命名空间里
Response.Clear();
Response.AddHeader("Content-Disposition", "filename=" + file.Name);
Response.AddHeader("Content-Length", file.Length.ToString());
string fileExtension = file.Extension;

// 根据文件后缀指定文件的Mime类型
switch (fileExtension)
{
case ".mp3":
Response.ContentType = "audio/mpeg3";
break;
case "mpeg":
Response.ContentType = "video/mpeg";
break;
case "jpg":
Response.ContentType = "image/jpeg";
break;
case "........等等":
Response.ContentType = "....";
break;
default:
Response.ContentType = "application/octet-stream";
break;
}
Response.WriteFile(file.FullName);
Response.End();
}
[/code]

4) 最后一步就是在IIS里增加一个应用程序扩展。在“默认网站”->“属性”->“主目录”->“配置”。在弹出的“应用程序配置”窗口里按“添加”,在弹出的“添加/编辑应用程序扩展名映射”窗口里“可执行文件”选择C:\WINDOWS\Microsoft.NET\Framework\v1.1.4322\aspnet_isapi.dll,在扩展名里输入“.rar”,然后确定即可。

5) 在IE里输入http://193.100.100.56/TestWebSolution/WebApplication1/test.rar,会立即跳转到http://193.100.100.56/TestWebSolution/WebApplication1/WebForm1.aspx,然后按WebForm1.aspx的“下载”按钮就可以下载test.rar了。

6) 当然,这里只按例子给个思路,完全可以再根据自身情况扩展。下面有几个参考的资源文章:
http://www.9seek.com/news/show.aspx?id=745&cid=12
http://www.9seek.com/news/show.aspx?id=521&cid=12
http://www.9seek.com/news/show.aspx?id=520&cid=12
http://msdn.microsoft.com/asp.net/using/building/web/default.aspx?pull=/library/en-us/dnaspp/html/URLRewriting.asp

3.
对每个 IP 访问量实时监控
By 蝈蝈俊
From http://blog.joycode.com/ghj/posts/26447.aspx

一些下载网站内容的机器人程序,写得很不好,会疯狂的刷比较耗时的页面,这会对正常用户的访问带来很大的麻烦。避免这个问题,就需要实时对用户访问量进行监控。


以前自己用VC++6.0写了个ISAPI filter .就是做实时限制,实时封杀IP功能的。

当时是参照下面的文章写的:
http://www.codeguru.com/Cpp/I-N/isapi/filters/article.php/c1303

(按:作者提供的代码大意如下
以下内容为程序代码:


EXPORTS
HttpFilterProc
GetFilterVersion

// 每个ip的信息记录类
class CIpInfo
{
public:
CIpInfo();
virtual ~CIpInfo();

// ip地址
long m_cIPAddress; // ip address

// ip计数,如果是当前访问记录中用,是这个ip一定时间内访问次数
// 如果用在封杀记录中,是还要被封杀时间的记录,这个记录是单位时间的倍数
int m_idegree;

};




然后是核心部分

以下内容为程序代码:

// CSDNIPPOLICY.H - Header file for your Internet Server
// CsdnIpPolicy

// The html page showing access denied.
#define NO_ACCESS "NoAccess.htm"

//#define INITIALIZE_FILE "C:\\TEMP\\csdnippolicy.xml"
//日志文件保存目录
#define LOG_PATH "LOG\\"

//配置文件
#define INI_FILE "CSDNIPPOLICY.INI"
// 多长间隔处理一次;
#define TIMELIMIT 3*60*1000

#include "resource.h"
#include "IpInfo.h"



typedef deque IP_LIST;


class CCsdnIpPolicy : public CHttpFilter
{
protected:
VOID TerminateCache();
static BOOL TimeSwitch();
BOOL Initialize();
static BOOL ValidateIPAddress(const long pszIPAddress, OUT BOOL* pfValid);
// 记录访问ip列表
static IP_LIST m_request_ip_list;
// 记录被限制ip列表
static IP_LIST m_force_out_ip_list;
// 不通过这个限制的ip
static IP_LIST m_free_ip_list;
// 缓存保护锁
// 保护 m_force_out_ip_list 的锁
static CRITICAL_SECTION m_force_out_CacheLock;
// 保护 m_request_ip_list 的锁
static CRITICAL_SECTION m_request_CacheLock;
//前一个时间的锁
static CRITICAL_SECTION m_pre_time_CacheLock;
static DWORD m_pre_time;
static CString m_Curr_Path;
public:
static BOOL AddIPAddressToRequestList(const long pszIPAddress);
CCsdnIpPolicy();
~CCsdnIpPolicy();

// Overrides
// ClassWizard generated virtual function overrides
// NOTE - the ClassWizard will add and remove member functions here.
// DO NOT EDIT what you see in these blocks of generated code !
//{{AFX_VIRTUAL(CCsdnIpPolicy)
public:
virtual BOOL GetFilterVersion(PHTTP_FILTER_VERSION pVer);
virtual DWORD OnUrlMap(CHttpFilterContext* pCtxt, PHTTP_FILTER_URL_MAP pMapInfo);
//}}AFX_VIRTUAL

//{{AFX_MSG(CCsdnIpPolicy)
//}}AFX_MSG
};



在ASP.net中, 是可以写一个 httpModules 程序来实现实时监控。原因看下图说明的asp.net执行机制。

其实 asp.net 中的 Session 等都是用这个机制实现的。具体可以看你本机的 machine.config 文件。

这种方式下的代码在:
http://www.ghj1976.net/OpenSource/Project/IPPolicy/Code/IpPolicy.rar

需要注意的地方:

第一:基于 httpModules 的这个程序,只对
ASP.net 系列的文件(比如 *.aspx *.asmx)有效。也就是由 aspnet_isapi.dll? 来解析的文件。
对 *.html *.asp *.jpg 等这些文件无效。
要想对这些文件有效,请用前一个 ISAPI filter 程序。

第二:基于 httpModules 的这个程序,在一个站点有多个虚拟目录的情况下,各个虚拟目录都是一个独立的运算单元,相关之间没有关系。各个虚拟目录的实时记录都是不通用的。

This page is powered by Blogger. Isn't yours?