Dim message, stream
Set message = CreateObject("CDO.Message")
message.MimeFormatted = True
'0 - cdoSuppressNone
message.CreateMHTMLBody "http://www.microsoft.com",0, "" ,""
Set stream = message.GetStream()
'2 - adSaveCreateOverWrite
stream.SaveToFile "C:\Test.mht",2
Set message = Nothing
stream.Close
Set stream = Nothing
小trick.. 即使有了ADO.Net,ADO还是很有用的……
Dino写了三篇文章介绍Script Callback:
Script Callbacks in ASP.NET
http://msdn.microsoft.com/msdnmag/issues/04/08/CuttingEdge/
Custom Script Callbacks in ASP.NET
http://msdn.microsoft.com/msdnmag/issues/05/01/CuttingEdge/
Implications of Script Callbacks in ASP.NET
http://msdn.microsoft.com/msdnmag/issues/04/12/CuttingEdge/
我在ASP.Net 1.1里写了一个Page的继承类,可以像ASP.Net 2.0一样使用Script Callback:
Implementing Callback framework in ASP.Net 1.1
http://blog.joycode.com/tingwang/articles/47304.aspx
没什么难度,估计别人也早已经写过,权当灌水……
To implement the script callback framework in ASP.Net 1.1, we need the following things:
1. The ICallbackEventHandler.
2. The Page that has incorporated the script callback feature.
3. The client side script for call back.
The interface ICallbackEventHandler is simple:
using System;
namespace ClientPopUp
{
// This interface must be implemented by any control or page that needs postback.
public interface ICallbackEventHandler
{
string RaiseClientCallbackEvent(string eventArgument);
}
}
The page can inherit from “System.Web.UI.Page”:
using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
namespace ClientPopUp
{
public class CallbackPage : System.Web.UI.Page
{
protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// __CALLBACKID will be in the Request dictionary with script callback.
if (Request["__CALLBACKID"] != null)
{
_isCallback = true;
handleCallback();
}
}
private void handleCallback()
{
// Get the UniqueID of the control.
// I have not tested this with controls put in INamingContainer.
// It may not work properly.
Control _control;
string controlUniqueID = Request["__CALLBACKID"];
if (controlUniqueID == "__PAGE")
{
_control = this;
}
else
{
_control = FindControl(controlUniqueID);
}
// Get the reference to the interface.
ICallbackEventHandler callbackHandler = _control as ICallbackEventHandler;
if (callbackHandler != null)
{
Response.Clear();
string result;
try
{
// Fire the callback handler method.
result = callbackHandler.RaiseClientCallbackEvent(Request["____CALLBACKPARAM"]);
// 's' for success.This will be further processed at client side.
Response.Write('s');
}
catch (Exception ex)
{
// 'e' for Exception.
Response.Write('e');
result = ex.Message;
}
// Write back to client.
Response.Write(result);
// Avoid any caching at client side.
Response.Cache.SetExpires(DateTime.Now);
Response.End();
}
}
// Is client script callback?
private bool _isCallback = false;
public bool IsCallback
{
get {return _isCallback;}
}
// JavaScript file contains the client callback functions
private const string CALLBACKSCRIPT_KEY = "Callback.js";
// Get the client side callback JavaScript function name.
// Then we can attach the function to any client side events.
public string GetCallbackEventReference(
Control _control, // Target control that handles callback at server side.
string argument, // String argument.
string clientCallback, // Client side callback function to process the result.
// Usually this function operates DOM.
string context, // Optional context.
string clientErrorCallback, // Optional client side error handler.
bool useAsync // Sync or Async ?
)
{
string target;
if (_control is ICallbackEventHandler)
{
if (_control is Page)
{
target = "__PAGE"; // Page needs special handling.
}
else
{
target = _control.UniqueID;
}
}
else
{
throw new ArgumentException("The control must implement ICallbackEventHandler interface.");
}
if ((clientCallback == null) || (clientCallback == string.Empty))
{
throw new ArgumentException("The clientCallback argument cannot be null or empty");
}
if (((Request != null)) && Request.Browser.JavaScript)
{
// Register the callback initialization scripts.
if (!IsClientScriptBlockRegistered("CallbackInitializationScript"))
{
RegisterClientScriptBlock("CallbackInitializationScript",
String.Format("<script language=JavaScript src=\"{0}/scripts/{1}\" type=\"text/javascript\"></script>", Request.ApplicationPath, CALLBACKSCRIPT_KEY));
}
if (!IsStartupScriptRegistered("PageCallbackScript"))
{
RegisterStartupScript("PageCallbackScript", "<script language=JavaScript>var pageUrl='" + Request.Url.PathAndQuery + "';\r\n WebForm_InitCallback();</script>");
}
}
else
{
throw new NotSupportedException("Browser does not support JavaScript?");
}
if (argument == null)
{
argument = "null";
}
else if (argument.Length == 0)
{
argument = "\"\"";
}
if (context == null)
{
context = "null";
}
else if (context.Length == 0)
{
context = "\"\"";
}
// Constructor the client side callback function reference.
string[] textArray1 = new string[13] { "WebForm_DoCallback('", target, "',", argument, ",", clientCallback, ",", context, ",", (clientErrorCallback == null) ? "null" : clientErrorCallback, ",", useAsync ? "true" : "false", ")" } ;
return string.Concat(textArray1);
}
}
}
The client side callback script can be the most difficult part but we can borrow it from ASP.Net 2.0:
// Global callback object. It seems that this cannot process simultaneous Async callbacks.
var __callbackObject = new Object();
function WebForm_DoCallback(eventTarget, eventArgument, eventCallback, context, errorCallback, useAsync) {
re = new RegExp("\\x2B", "g");
var postData = __theFormPostData +
"__CALLBACKID=" + eventTarget +
"&__CALLBACKPARAM=" + escape(eventArgument).replace(re, "%2B");
// For non MS browsers, we can use the "XMLHttpRequest" object.
if (__nonMSDOMBrowser) {
var xmlRequest = new XMLHttpRequest();
if (pageUrl.indexOf("?") != -1) {
xmlRequest.open("GET", pageUrl + "&" + postData, false);
}
else {
xmlRequest.open("GET", pageUrl + "?" + postData, false);
}
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(null);
response = xmlRequest.responseText;
if (response.charAt(0) == "s") {
if (eventCallback != null) {
eventCallback(response.substring(1), context);
}
}
else {
if (errorCallback != null) {
errorCallback(response.substring(1), context);
}
}
}
// For IE, we use XMLHTTP.
else {
var xmlRequest = new ActiveXObject("Microsoft.XMLHTTP");
// Hook the readystatechange event.
xmlRequest.onreadystatechange = WebForm_CallbackComplete;
__callbackObject.xmlRequest = xmlRequest;
__callbackObject.eventCallback = eventCallback;
__callbackObject.context = context;
__callbackObject.errorCallback = errorCallback;
var usePost = false;
// POST or use URL query string?
if (pageUrl.length + postData.length + 1 > 2067) {
usePost = true;
}
if (usePost) {
xmlRequest.open("POST", pageUrl, useAsync);
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send(postData);
}
else {
if (pageUrl.indexOf("?") != -1) {
xmlRequest.open("GET", pageUrl + "&" + postData, useAsync);
}
else {
xmlRequest.open("GET", pageUrl + "?" + postData, useAsync);
}
xmlRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xmlRequest.send();
}
}
}
function WebForm_CallbackComplete() {
if (__callbackObject.xmlRequest.readyState == 4) {
response = __callbackObject.xmlRequest.responseText;
// Success or Exception from the server side?
if (response.charAt(0) == "s") {
if (__callbackObject.eventCallback != null) {
// Invoke the client side callback function to manipulate DOM.
__callbackObject.eventCallback(response.substring(1), __callbackObject.context);
}
}
else {
if (__callbackObject.errorCallback != null) {
__callbackObject.errorCallback(response.substring(1), __callbackObject.context);
}
}
}
}
var __nonMSDOMBrowser = (window.navigator.appName.toLowerCase().indexOf('explorer') == -1);
var __theFormPostData = "";
var theForm = document.forms[0];
function WebForm_InitCallback() {
count = theForm.elements.length;
var element;
re = new RegExp("\\x2B", "g");
// Prepare the FORM data we will send to the server.
for (i = 0; i < count; i++) {
element = theForm.elements[i];
if (element.tagName.toLowerCase() == "input") {
__theFormPostData += element.name + "=" + element.value.replace(re, "%2B") + "&";
}
else if (element.tagName.toLowerCase() == "select") {
selectCount = element.children.length;
for (j = 0; j < selectCount; j++) {
selectChild = element.children[j];
if ((selectChild.tagName.toLowerCase() == "option") && (selectChild.selected == true)) {
__theFormPostData += element.name + "=" + selectChild.value.replace(re, "%2B") + "&";
}
}
}
}
}
That’s all. The following is a test page. For more information about the callback framework, please refer to the document on ASP.Net 2.0.
<%@ Page language="c#" Inherits="ClientPopUp.CallbackPage" %>
<%@ Implements Interface="ClientPopUp.ICallbackEventHandler" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<HTML>
<HEAD>
<title>WebForm1</title>
<script language='JavaScript'>
function ClientSideCallBack(responseText, context) {
alert(responseText);
}
</script>
</HEAD>
<body>
<form id="Form1" method="post" runat="server">
<INPUT id="Button1" type="button" value="Button" name="Button1" runat="server">
</form>
<script language="C#" runat="server">
public string RaiseClientCallbackEvent(string eventArgument)
{
return DateTime.Now.ToString();
}
private void Page_Load(object sender, System.EventArgs e)
{
this.Button1.Attributes["onClick"] = GetCallbackEventReference(this,"'Dummy Argument'","ClientSideCallBack",null,null,true);
}
</script>
</body>
</HTML>
经常看到用户要在ASP.Net 中跑一些需要很长时间才能返回的任务(如DTS Package等等)。这样一来一个页面可能很长时间无法Render到客户端,而使用户对当前任务的状态产生疑惑。
MSDN上看到了一些解决方案:
Build Your ASP.NET Pages on a Richer Bedrock
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnvs05/html/BedrockASPNET.asp?frame=true
Asynchronous Wait State Pattern in ASP.NET
http://msdn.microsoft.com/msdnmag/issues/03/12/DesignPatterns/
它们都将用户重定向到一个等待页面,然后再重定向到执行任务的页面。任务的执行过程中,用户看到的是等待页面,等任务执行完,整个执行任务的页面就会彻底返回到客户端。这种做法还是发现有个问题,因为任务执行过程中IE和服务器之间没有任何通讯,IIS服务器会将非活动的连接关闭。这个超时值虽然可以在网站属性(Web Site)里设得更长一点,但是这个超时太大会对整个网站的性能会有影响。
这里可以采用另外一种方法:在服务器端另起一个线程执行任务,并通过客户端脚本不停刷新页面来给用户反馈,只到任务运行结束。这样也可以防止同一个任务被同时执行多次:
ASP.Net Lengthy Task Visual Feedback Page
http://blog.joycode.com/tingwang/articles/47292.aspx
First let’s implement a class to represent a long run task. It has some properties which can be used to evaluate the status of the task:
using System;
using System.Threading;
namespace LongRunTask
{
/// <summary>
/// Summary description for LengthyTask.
/// </summary>
public class LengthyTask
{
// A flag to indicate whether the task has run at least once.
private bool _firstRunFinished = false;
public bool FirstRunFinished
{
get
{
return _firstRunFinished;
}
}
// A flag to indicate whether the task is running.
private bool _running = false;
public bool Running
{
get
{
return _running;
}
}
// A flag to indicate whether the last task succeeded or not.
public bool _lastTaskSuccessful = true;
public bool LastTaskSuccessful
{
get
{
if (_lastFinishTime == DateTime.MinValue)
throw new InvalidOperationException("The task has not finished even once.");
return _lastTaskSuccessful;
}
}
// To store any exception generated during the task.
private Exception _exceptionOccured = null;
public Exception ExceptionOccured
{
get
{
return _exceptionOccured;
}
}
private DateTime _lastStartTime = DateTime.MinValue;
public DateTime LastStartTime
{
get
{
if (_lastStartTime == DateTime.MinValue)
throw new InvalidOperationException("The task has not started even once.");
return _lastStartTime;
}
}
private DateTime _lastFinishTime = DateTime.MinValue;
public DateTime LastFinishTime
{
get
{
if (_lastFinishTime == DateTime.MinValue)
throw new InvalidOperationException("The task has not finished even once.");
return _lastFinishTime;
}
}
// Start the task
public void RunTask()
{
// Only one thread is allowed to enter here.
lock (this)
{
if (!_running)
{
_running = true;
_lastStartTime = DateTime.Now;
Thread t = new Thread(new ThreadStart(ThreadWork));
t.Start();
}
else
{
throw new InvalidOperationException("The task is already running!");
}
}
}
public void ThreadWork()
{
try
{
// Suppose that we need to run the DTS package here.
// Replace the following line with your code.
Thread.Sleep(20000);
// Setting successful flag.
_lastTaskSuccessful = true;
}
catch (Exception e)
{
// Failed.
_lastTaskSuccessful = false;
_exceptionOccured = e;
}
finally
{
_running = false;
_lastFinishTime = DateTime.Now;
if (!_firstRunFinished) _firstRunFinished = true;
}
}
}
}
In the ASP.Net page, we can store this long run task object in Application state or Cache state, so that we can ensure it is unique to the whole application. Then in the page life cycle, we can query the status of the task and provide visual feedback accordingly to the end user. This also prevents the situation that some users try to run the task simultaneously. The page will refresh itself with client side script if the task is running.
private void Page_Load(object sender, System.EventArgs e)
{
task = Cache["LengthyTask"] as LengthyTask;
if (task == null)
{
task = new LengthyTask();
Cache["LengthyTask"] = task;
}
// The task is already running.
if (task.Running)
{
PreparePageWithTaskRunning();
}
else
{
PreparePageWithTaskNotRunning();
}
}
protected System.Web.UI.WebControls.Button Button1;
protected System.Web.UI.WebControls.Label Label1;
private LengthyTask task;
private void Button1_Click(object sender, System.EventArgs e)
{
if (!task.Running)
{
task.RunTask();
PreparePageWithTaskRunning();
}
}
private void PreparePageWithTaskRunning()
{
Button1.Enabled = false;
Label1.Text = "The task is running now.<br>" +
"It started at " + task.LastStartTime.ToString() + "<br>" +
(DateTime.Now - task.LastStartTime).Seconds + " seconds have elapsed";
// Register the script to refresh the page every 3 seconds.
RegisterStartupScript("key",@"<script language=javascript>
window.setTimeout('document.location.replace(document.location.href); ',3000);
</script>");
}
private void PreparePageWithTaskNotRunning()
{
Label1.Text = "The task is not running.";
if (task.FirstRunFinished)
{
Label1.Text += "<br>Last time it started at " + task.LastStartTime.ToString() + "<br>" +
"and finished at " + task.LastFinishTime.ToString() + "<br>";
if (task.LastTaskSuccessful)
{
Label1.Text += "It succeeded.";
}
else
{
Label1.Text += "It failed.";
if (task.ExceptionOccured != null)
{
Label1.Text += "<br>The exception was: " + task.ExceptionOccured.ToString();
}
}
}
}
有的服务器有多个IP地址。这样的服务器host CAO的时候会有一个问题,Remoting Framework可能会将CAO绑定到一个错误的IP地址,例如公网的用户可能得到一个服务器在私网的IP,这样在调用CAO的方法时会失败。同样的情况也发生在一个Server Activated Object的方法返回一个MarshalByRefObject的时候。对于这样的情况,可以通过Channel的“bindTo”属性将一个Channel强制绑定给一个IP,例如公网IP,可是这样一来,私网的用户就无法使用同一个Channel了。
下面有几种解决方案:
1. 将Channel绑定到机器名(使用“machineName”),而非IP。但这样要求我们不同网段的客户都能通过同一个DNS名找到这个服务器。
2. 为服务器每一个IP都开一个Channel,并使用“bindTo”。
3. 通过服务器端的Sink取得Client端的IP,并通过手动配置的类似路由表一样的对应表,通过客户端的IP来选择一个正确的服务器IP。我们可以用TrackingHandler在服务器端Marshal MarshalByRefObject的时候用一个正确的IP。
How to get the IP address of the Remoting Client on Remoting Server
http://blog.joycode.com/tingwang/articles/39610.aspx
4. 通过服务器端的Sink以及Reflection,取得收到请求的服务器端地址,并通过TrackingHandler来指定正确的地址。
How to automatically bind the correct server IP address to a CAO ObjectRef under multi network adapter environment.
http://blog.joycode.com/tingwang/articles/47285.aspx
5. 使用一个SAO作为类工厂,给服务器端的SAO方法传一个该使用的服务器IP地址,然后同样通过CallContext来传递这个值给TrackingHandler。
这样的问题不存在于SAO中,因为SAO的地址都是在客户端指定的,而不需要在服务器端生成。
To bind the correct server IP address to a CAO object automatically under a multi network adapter environment, we need to achieve the following 2 steps:
1. Retrieve the correct server IP address that accepts the request. Then save it in the CallContext.
2. Retrieve the value from the CallContext later and assign it to the ObjRef.
First of all, we need to retrieve the server IP address that accepts the request. It seems that the Remoting Framework does not expose any public interface for us. Then we need to use Reflection to retrieve the IP address. The following server side sink is modified from another sample:
How to get the IP address of the Remoting Client on Remoting Server
http://blog.joycode.com/felix/articles/39610.aspx
using System;
using System.Collections;
using System.IO;
using System.Runtime.Remoting;
using System.Runtime.Remoting.Messaging ;
using System.Runtime.Remoting.Channels;
using System.Threading;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
namespace ServerProcess
{
public class ClientIPServerSinkProvider: IServerChannelSinkProvider
{
private IServerChannelSinkProvider next = null;
public ClientIPServerSinkProvider(IDictionary properties, ICollection providerData)
{
//Do not need to process any properties here.
}
public ClientIPServerSinkProvider()
{
//Default constructor
}
public void GetChannelData (IChannelDataStore channelData)
{
}
public IServerChannelSink CreateSink (IChannelReceiver channel)
{
IServerChannelSink nextSink = null;
if (next != null)
{
nextSink = next.CreateSink(channel);
}
return new ClientIPServerSink(nextSink);
}
public IServerChannelSinkProvider Next
{
get { return next; }
set { next = value; }
}
}
public class ClientIPServerSink : BaseChannelObjectWithProperties, IServerChannelSink, IChannelSinkBase
{
private IServerChannelSink _next;
public ClientIPServerSink (IServerChannelSink next)
{
_next = next;
}
public void AsyncProcessResponse(IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers, Stream stream)
{
//Ignored.
}
public Stream GetResponseStream(IServerResponseChannelSinkStack sinkStack, object state, IMessage msg, ITransportHeaders headers)
{
return null;
}
public ServerProcessing ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, out IMessage responseMsg, out ITransportHeaders responseHeaders, out Stream responseStream)
{
if (_next != null)
{
try
{
//System.Runtime.Remoting.Channels.ServerChannelSinkStack
Type _type = sinkStack.GetType();
FieldInfo _fieldInfo = _type.GetField("_stack", BindingFlags.Instance | BindingFlags.NonPublic);
object _fieldValue = _fieldInfo.GetValue(sinkStack);
//System.Runtime.Remoting.Channels.ServerChannelSinkStack.SinkStack
_type = _fieldValue.GetType();
_fieldInfo = _type.GetField("State", BindingFlags.Instance | BindingFlags.Public);
_fieldValue = _fieldInfo.GetValue(_fieldValue);
//System.Runtime.Remoting.Channels.Tcp.TcpServerSocketHandler
_type = _fieldValue.GetType();
_fieldInfo = _type.GetField("NetSocket",BindingFlags.Instance | BindingFlags.NonPublic);
_fieldValue = _fieldInfo.GetValue(_fieldValue);
CallContext.SetData("ServerIPAddress",((IPEndPoint)(((Socket)_fieldValue).LocalEndPoint)).Address);
}
catch (Exception exc)
{
Exception ex = new Exception("Exception in Reflection in ClientIPServerSink",exc);
throw ex;
}
IPAddress ip = requestHeaders[CommonTransportKeys.IPAddress] as IPAddress;
CallContext.SetData("ClientIPAddress",ip);
ServerProcessing spres = _next.ProcessMessage (sinkStack,requestMsg, requestHeaders,requestStream,out responseMsg,out responseHeaders,out responseStream);
return spres;
}
else
{
responseMsg=null;
responseHeaders=null;
responseStream=null;
return new ServerProcessing();
}
}
public IServerChannelSink NextChannelSink
{
get {return _next;}
set {_next = value;}
}
}
}
With the correct server IP address in the CallContext, we can proceed to the next step. We can make use of the ITrackingHandler to assign the address to the ObjRef object. ITrackingHandler. MarshaledObject method will be called after an MBR object is marshaled:
using System;
using System.Runtime.Remoting;
using System.Runtime.Serialization;
using System.Runtime.Remoting.Services;
using System.Runtime.Remoting.Messaging;
using System.Runtime.Remoting.Channels;
using System.Net;
namespace ServerProcess
{
public class ChangeIPTrackingHandler : ITrackingHandler
{
// Notify a handler that an object has been marshaled.
public void MarshaledObject(Object obj, ObjRef or)
{
//Check the type of the object. We only care about the CAO object here:
if (!(obj is ClassLibRemotable.CAOClass)) return;
IPAddress ip = CallContext.GetData("ServerIPAddress") as IPAddress;
if ((ip == null) ||(or.ChannelInfo == null))
return;
string strAddress = ip.ToString();
for (int i = 0; i < or.ChannelInfo.ChannelData.Length; i++ )
{
// Check for the ChannelDataStore object that we don't want to copy
if(or.ChannelInfo.ChannelData[i] is ChannelDataStore)
{
for (int j=0; j < ((ChannelDataStore)(or.ChannelInfo.ChannelData[i])).ChannelUris.Length;j++)
{
string uri = ((ChannelDataStore)(or.ChannelInfo.ChannelData[i])).ChannelUris[j];
// this will get the first part of the uri
int nOffset = uri.IndexOf( "//" ) + 2;
string strNewUri = uri.Substring(0, nOffset );
strNewUri += strAddress;
nOffset = uri.IndexOf( ":", nOffset );
strNewUri += uri.Substring( nOffset, uri.Length - nOffset );
((ChannelDataStore)(or.ChannelInfo.ChannelData[i])).ChannelUris[j] = strNewUri;
}
}
}
}
public void DisconnectedObject(object o)
{
}
public void UnmarshaledObject(object o, ObjRef oref)
{
}
}
}