2004年07月12日

For various reasons, you might not know until run time what templates you need or what text or controls should be in them. In that case, you need to be able to create the templates dynamically (in code).

Note   You can also create templates as Web Forms user controls and bind them dynamically to controls on your page. For details, see Creating a Templated User Control.

You can create templates in code for all controls that use templates: the DataList, Repeater, and DataGrid controls. For the DataGrid control, you use templates to define columns instead of the row-like templates for the other two controls.

Note   There are a few differences when creating template columns for the DataGrid control. For details, see Creating Templates Programmatically in the DataGrid Control.

Creating the Template Class

To create dynamic templates, you create a template class that you can then instantiate when needed.

Note   For background on creating classes in Visual Basic .net, see Understanding Classes. For similar information for Visual C# .net, see class.

To create a template class

  1. Create a new class that implements the ITemplate interface of the System.Web.UI namespace.
  2. Optionally, pass into the class’s constructor a value that the class can use to determine what type of template to create (ItemTemplate, AlternatingItemTemplate, and so on).
    Tip   A type-safe way to pass the template type to the constructor is to add a parameter to the constructor with the type ListItemType. The ListItemType enumeration defines the possible template types for the Repeater, DataList, and DataGrid controls.

  3. In the class, implement the InstantiateIn method (the only member of the ITemplate interface). This method provides a way to insert an instance of text and controls into the specified container.
  4. In the InstantiateIn method, create the controls for the template item, set their properties, and then add them to the parent’s Controls collection. You can access the parent control via the reference passed to the InstantiateIn method.
    Note   You cannot directly add static text to the Controls collection, but you can create controls like the Literal control or the LiteralControl control, set their Text properties, and then add those controls to the parent collection.

    The following example illustrates a complete template class that displays some static text (“Item number:”) and a counter. The counter is maintained as a shared or static value (depending on what language you are using) called itemcount for the class and incremented each time a new item is created.

    The class defines an explicit constructor that accepts a ListItemType enumeration value to indicate what type of template is being created. Depending on what type of template is being created, the code creates different types of controls and adds them to the Controls collection of the parent control. The end result is an HTML table with a different background color for the alternating item template.

    ' Visual Basic Private Class MyTemplate Implements ITemplate Shared itemcount As Integer = 0 Dim TemplateType As ListItemType Sub New(ByVal type As ListItemType) TemplateType = type End Sub Sub InstantiateIn(ByVal container As Control) _ Implements ITemplate.InstantiateIn Dim lc As New Literal() Select Case TemplateType Case ListItemType.Header lc.Text = "<TABLE border=1><TR><TH>Items</TH></TR>" Case ListItemType.Item lc.Text = "<TR><TD>Item number: " & itemcount.ToString _ & "</TD></TR>" Case ListItemType.AlternatingItem lc.Text = "<TR><TD bgcolor=lightblue>Item number: " _ & itemcount.ToString & "</TD></TR>" Case ListItemType.Footer lc.Text = "</TABLE>" End Select container.Controls.Add(lc) itemcount += 1 End Sub End Class // C# public class MyTemplate : ITemplate { static int itemcount = 0; ListItemType templateType; public MyTemplate(ListItemType type) { templateType = type; } public void InstantiateIn(System.Web.UI.Control container) { Literal lc = new Literal(); switch( templateType ) { case ListItemType.Header: lc.Text = "<TABLE border=1><TR><TH>Items</TH></TR>"; break; case ListItemType.Item: lc.Text = "<TR><TD>Item number: " + itemcount.ToString() + "</TD></TR>"; break; case ListItemType.AlternatingItem: lc.Text = "<TR><TD bgcolor=lightblue>Item number: " + itemcount.ToString() + "</TD></TR>"; break; case ListItemType.Footer: lc.Text = "</TABLE>"; break; } container.Controls.Add(lc); itemcount += 1; } }

Using the Dynamic Template

When you have a dynamic template available, you can instantiate it in code.

Note   To work with a dynamic template as a column in a DataGrid control, see Creating Templates Programmatically in the DataGrid Control.

To use a dynamic template

  1. Create an instance of your dynamic template, passing it an item type value if appropriate.
  2. Assign the instance to one of the template properties of the Repeater or DataList control: ItemTemplate, AlternatingItemTemplate, HeaderTemplate, and so on.

    The following example shows how to use the dynamic template with a Repeater control. In this example, the templates are instantiated while the page is being loaded and before the control is bound to its data source.

    ' Visual Basic Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load  Repeater1.HeaderTemplate = New MyTemplate(ListItemType.Header)  Repeater1.ItemTemplate = New MyTemplate(ListItemType.Item)  Repeater1.AlternatingItemTemplate = _  New MyTemplate(ListItemType.AlternatingItem)  Repeater1.FooterTemplate = New MyTemplate(ListItemType.Footer) SqlDataAdapter1.Fill(DsCategories1) Repeater1.DataBind() End Sub // C# private void Page_Load(object sender, System.EventArgs e) {  Repeater1.HeaderTemplate = new MyTemplate(ListItemType.Header);  Repeater1.ItemTemplate = new MyTemplate(ListItemType.Item);  Repeater1.AlternatingItemTemplate =   new MyTemplate(ListItemType.AlternatingItem);  Repeater1.FooterTemplate = new MyTemplate(ListItemType.Footer); sqlDataAdapter1.Fill(dsCategories1); Repeater1.DataBind(); }

Adding Data Binding to Templates

There are various ways to get access to data from within a template class, depending on how you have created the class. A good way is one in which the page architecture itself implements data binding — when you add controls to the template, you also add a handler for their DataBinding event. This event will be raised after the template item has been created with all its controls, and it provides you with an opportunity to fetch data and use it in a control.

Note   You cannot embed a data-binding expression as a string when creating controls in the template, as you do when defining templates at design time, because data-binding expressions are converted into code at a stage of page processing that occurs before your template is created.

In the handler for the DataBinding event, you have an opportunity to manipulate the contents of the control. Typically (but not necessarily), you fetch data from somewhere and assign it to the control’s Text property.

Note   For background on data binding in Web Forms pages, see Web Forms Data Binding.

To add data binding to a dynamic template, you must do the following:

  • Add a data-binding event handler to the controls you create in the template.
  • Create the handler that you are binding to. In the handler, get the data that you want to bind to and assign it to the appropriate property of the control being bound.

To add the data-binding event handler

  • After creating a control in the dynamic template, use standard commands to bind the control’s DataBinding event to a method that you will create later.
    Note   For details on how to add event handlers dynamically, see AddHandler and RemoveHandler (for Visual Basic) and Events Tutorial (for Visual C#).

    The following example shows code from the template class illustrating how you can bind a newly created control to a method called TemplateControl_DataBinding.

    ' Visual Basic Dim lc As New Literal() Case ListItemType.Item lc.Text = "<TR><TD>"  AddHandler lc.DataBinding, AddressOf TemplateControl_DataBinding // C# Literal lc = new Literal(); case ListItemType.Item: lc.Text = "<TR><TD>";  lc.DataBinding += new EventHandler(TemplateControl_DataBinding); break;

    Note that in this example, the text you add to the literal control’s Text property is different than in the previous example. It contains only the beginning of the table row and cell for the item template. You will complete the cell and row in the data-binding event handler.

The next step is to create the event handler that will be called when the control is data bound.

To create the handler for the DataBinding event

  1. Create a method that is part of your template class as a peer of the class’s other methods (such as InstantiateIn). The handler’s name must match the name you used when binding the event earlier. The method should have the following signature:
    ' Visual Basic Private Sub TemplateControl_DataBinding(ByVal sender As Object, _ ByVal e As System.EventArgs) // C# private void TemplateControl_DataBinding(object sender, System.EventArgs e)

  2. Get a reference to the DataItem object containing the data for the current template item, by doing the following:
    1. Get a reference to the template item. You create a variable to hold the reference and then assign it the value you get from your control’s NamingContainer property.
    2. Use that reference to get the naming container’s (the template item’s) DataItem property.
    3. Extract the individual data element (data column, for example) from the DataItem object and use it to set a property of the control you are binding.

      The following code illustrates one way to perform data binding within a dynamic template. It shows a complete data-binding event handler for Literal controls being created in a template for a Repeater control.

      ' Visual Basic Private Sub TemplateControl_DataBinding(ByVal sender As Object, _ ByVal e As System.EventArgs) Dim lc As Literal  lc = CType(sender, Literal)  Dim container As RepeaterItem  container = CType(lc.NamingContainer, RepeaterItem)  lc.Text &= DataBinder.Eval(container.DataItem, "CategoryName") lc.Text &= "</TD></TR>" End Sub // C# private void TemplateControl_DataBinding(object sender, System.EventArgs e) { Literal lc;  lc = (Literal) sender;  RepeaterItem container = (RepeaterItem) lc.NamingContainer;  lc.Text += DataBinder.Eval(container.DataItem, "CategoryName"); lc.Text += "</TD></TR>"; }


      Note   If you have multiple types of controls in your templates, you would need to create a different data-binding event handler for each of the control types.

2004年07月05日

这是PPC程序
下面是一个类 ClassConnection里的代码
using System.Net.Sockets;
using System.Text;
public void SendFood(string strText)
  {
   TcpClient client;
   client = new TcpClient(“打印服务器名字,一般就是数据库的总服务器,这一定
是名字,不能是IP”, 10000); //10000是端口号
   StreamWriter writer = new StreamWriter(client.GetStream());
   writer.Write(strText + (char) 13);
   writer.Flush();
  }
下面是程序的代码
using System.Threading;
private void button1_Click(object sender, System.EventArgs e)
  {
      straa=”CHAT” ;//服务端接收的标识
      straa=straa+”|”+”要传的内容”;
      Thread t = new Thread(new ThreadStart(aaa));     //此处用线程 ,但没做
事务处理
      t.Start();
      Thread.Sleep(0);
}
private void aaa()
  {
   ClassConnection class1=new ClassConnection();
   class1.SendFood(straa);

  }
服务端的程序不用我再说明了吧,就是一个接收,然后打印,可以设好多台打印机,然
后分各个部门接收
“RuimeiSoft” <RuimeiSoft@msn.com> 写入邮件
news:useGRuiYEHA.2408@tk2msftngp13.phx.gbl
> 我需要的就是你这样的,可否告诉你怎么做的
>
> “可枫” <kefeng8888@hotmail.com> 写入邮件
> news:O0rDggiYEHA.2016@TK2MSFTNGP09.phx.gbl
> > 这个我前几天刚做完,我是做一个餐饮系统,要求服务员一点完菜就要在厨房打出
> 来,
> > 我用的是SOCKET,也就是说在服务器上做一个打印服务,接收SOCKET数据,然后根

> 所
> > 分配的打印机打出来。这样的优点是不点用资源。且PPC上的代码很少,就三五
行。
> > “RuimeiSoft” <RuimeiSoft@msn.com> 写入邮件
> > news:erP3d2OYEHA.3596@tk2msftngp13.phx.gbl
> > > 我想编写一个pdc应用程序,在实时操作的时候随时让打印机打印pdc的数据,但

> 保
> > 持
> > > pdc是无线连接的
> > > 如何实现啊
> > >
> > >
> >
> >
>
>

2004年05月31日

Introduction

ASP.NET web forms provide excellent event driven programming model to developers. This does simplifies the overall design of your application but poses some problems of its own. For example, in traditional ASP you can easily pass values from one ASP page to another ASP page using POST. The same thing is not possible in ASP.NET if you want to stick to web form model (i.e. Server side form and control processing.). There are, however, some ways that can be used to overcome this situation. This article examines various possibilities to do the same. More specifically we will cover how to pass values using querystring, how to use session variables to do the same and finally how to use Server.Transfer method to do that.

Using Querystring

Querystring is a day old mechanism to pass values across pages. The main advantage of this method is it is very simple. However, disadvantages are the values are visible in the browser address bar and you can not pass objects this way. This method is best suited when you want to pass small number of values that need not be secured from others. In order to implement this method you will follow these steps:

  • Create the web form with controls
  • Provide some button or link button that posts the form back
  • In the click event of the button create a string that holds URL for another
  • Add control values to this URL as querystring parameters
  • Response.Redirect to another form with this URL

Following code snippet shows how it works:

Source Web Form
private void Button1_Click (object sender, System.EventArgs e) { string url; url="anotherwebform.aspx?name=" + TextBox1.Text + "&email=" + TextBox2.Text; Response.Redirect(url); } 
Destination Web Form
private void Page_Load (object sender, System.EventArgs e) { Label1.Text=Request.QueryString["name"]; Label2.Text=Request.QueryString["email"]; } 

Using Session variables

This is yet another way to pass values across pages. Here you store control values in session variables and access them in another web form. However, as you know storing too much data in session can be an overhead on the server. So, you should use this method with care. Also, it requires some kind of clean up action from your side so that unwanted session variables are removed. The typical sequence of steps will be as follows:

  • Create the web form with controls
  • Provide some button or link button that posts the form back
  • In the click event of the button add session variables and set them to control values
  • Response.Redirect to another form
  • In that form access Session variables and remove them if necessary

Following code shows this in action:

Source Web Form
private void Button1_Click (object sender, System.EventArgs e) { //textbox1 and textbox2 are webform //controls Session["name"]=TextBox1.Text; Session["email"]=TextBox2.Text; Server.Transfer("anotherwebform.aspx"); } 
Destination Web Form
private void Page_Load (object sender, System.EventArgs e) { Label1.Text=Session["name"].ToString(); Label2.Text=Session["email"].ToString(); Session.Remove("name"); Session.Remove("email"); } 

Using Server.Transfer

This is somewhat complex but sophisticated method of passing values across pages. Here you expose the values you want to access in other pages as proprties of the page class. This methods require you to code extra properties that you can access in another web form. However, the efforts are worth considering. Overall this method is much cleaner and object oriented than earlier methods. The entire process works as follows:

  • Create the web form with controls
  • Create property Get procedures that will return control values
  • Provide some button or link button that posts the form back
  • In the button click event handler call Server.Transfer method that will transfer execution to the specified form
  • In the second form you can get a reference to the first form instance by using Context.Handler property. Then you will use the get properties we created to access the control values.

The code to accomplish this is somewhat complex and is shown below:

Source Web Form

Add following properties to the web form:

public string Name { get { return TextBox1.Text; } } public string EMail { get { return TextBox2.Text; } } 

Now, call Server.Transfer.

private void Button1_Click (object sender, System.EventArgs e) { Server.Transfer("anotherwebform.aspx"); } 
Destination Web Form
private void Page_Load (object sender, System.EventArgs e) { //create instance of source web form WebForm1 wf1; //get reference to current handler instance wf1=(WebForm1)Context.Handler; Label1.Text=wf1.Name; Label2.Text=wf1.EMail; } 

Summary

In this article we saw various ways to pass data between two ASP.NET web forms. The three methods viz. Querystring, Session and Server.Transfer provide ways to pass our values across pages. We also saw pros and cons of each method.

I hope you must have found this article useful. See you soon. Till then keep coding!

About the author

Name :

Bipin Joshi

Email :

webmaster at dotnetbips.com

Profile :

Bipin Joshi is the founder of DotNetBips.com. He runs a company called BinaryIntellect Consulting (www.binaryintellect.com) that provides software development, consulting and training services. He is also Microsoft MVP for .net.

2004年04月29日

http://chs.gotdotnet.com/quickstart/default.aspx

“快速入门教程”是理解 .net 框架技术为最前沿的开发人员提供什么内容的最快途径。在其中将可以找到关于 .net 框架技术最引人注目的功能的信息,包括如何使其立即为您或您的公司所用。若要开始使用,请访问下面的链接。

ASP.NET 快速入门
ASP.NET 是用于生成基于 Web 的应用程序的内容丰富的编程框架。它为开发人员和管理员提供出色的支持,提供改进的易用性、工具支持、可靠性、可缩放性、管理和安全性。“快速入门”讨论一系列示例代码和概念,讲授如何最大限度地利用这种功能强大的技术。

Windows 窗体快速入门
窗体是用于生成 Win 32 客户端应用程序的内容丰富的编程框架。它为 Win32 开发人员提供极好的支持,提供改进的易用性、一流的工具支持、坚如磐石的可靠性、更低廉的开发成本以及相关的所属权总成本的降低。“Windows 窗体快速入门”讨论一系列示例代码和概念,讲授如何最大限度地利用这种功能强大的技术。

常见任务快速入门
“如何…?”部分演示一些开发人员需要完成的最常见的任务,如读取和分析 XML、执行数据访问以及访问如 Active Directory 或 Win32 API 这样的服务。

2004年04月28日

    作者:Aaron Skonnard;陶刚翻译

  摘要:本文讲解微软ASP.NET Web服务方法(WebMethod)是如何提供高效率的建立Web服务的途径的。WebMethod可以把传统的微软.NET方法暴露为Web服务操作,支持HTTP、XML、XML Schema、SOAP和WSDL。WebMethod(.asmx)处理处理程序能自动地把输入的SOAP消息传递给适当的方法,并自动地把输入的XML元素串行化为相应的.NET对象。

  介绍

  目前在微软.NET中实现基于HTTP的Web服务有两种根本不同的途径。最底层的技术是编写一个插入.NET HTTP管道的自定义IHttpHandler类。这种途径要求你使用System.Web API来处理输入的HTTP消息,使用System.Xml API处理HTTP体中的SOAP封装。编写自定义处理程序要求你手工建立正确描述实现的WSDL文档。严格执行所有的这些操作要求你非常了解XML、XSD、SOAP和WSDL规范,但是这对于大多数开发者来说很困难。

  实现Web服务的效率更高的途径是使用微软ASP.NET WebMethods框架组件。ASP.NET为.asmx终结点(称为WebServiceHandler)发布了一个特定的IHttpHandler类,它提供必要的XML、XSD、SOAP和WSDL功能的范本文件。因为WebMethods框架组件把你从下层XML技术的复杂性中解放了出来,你能够快速聚焦于手头的业务问题。

  图1:灵活性和生产率之间的折衷

  在实现技术之间作出选择形成了图1中所示的灵活性和生产率之间的折衷。编写自定义的IhttpHandler给了你无限大的灵活性,但是要花费你很长时间编写、测试和调试代码。WebMethods框架组件使建立自己的Web服务和快速运行变得很容易,但是很明显你要受到该框架组件界限的限制。但是,在WebMethods框架组件不能提供你完全需要的信息的情况下,可以通过添加自己的附加功能来扩展该框架组件。

  通常,除非你已经掌握了XML、XSD、SOAP和WSDL并且原意承担直接处理它们的负担,最好不要专注于Web服务所需要的WebMethods框架组件。它提供了大多数Web服务端点(endpoints)所需要的基本服务,以及能把框架组件进行”弯曲”来适合你的精确的需要的一些有趣的可扩充能力。本文是基于这种假定讨论WebMethods如何工作的内部信息。如果你不太了解XML Schema和SOAP,可以参阅Understanding XML Schema和Understanding SOAP。

  WebMethods框架组件

  WebMethods框架组件循环地把SOAP消息映射到.NET类中的方法。这种功能的实现首先需要把你的方法注解为System.Web.Services名字空间中的[WebMethod]属性。例如,下面的.NET类包含四个方法,其中两个使用[WebMethod]属性注解了:

using System.Web.Services;

public class MathService
{
   [WebMethod]
   public double Add(double x, double y) {
      return x + y;
   }
   [WebMethod]
   public double Subtract(double x, double y) {
      return x – y;
   }
   public double Multiply(double x, double y) {
      return x * y;
   }
   public double Divide(double x, double y) {
      return x / y;
   }
}

  为了在WebMethods框架组件重使用这个类,你需要把这个类编译为一个部件(assembly)并把它复制到虚拟目录的bin目录中。在这个例子中,Add和Subtract方法可以被暴露作为Web服务操作,但是Multiply和Divide却不能(因为它们没有使用[WebMethod]标记)。

  你可以通过一个.asmx端点(endpoint)把Add和Subtract暴露为Web服务操作。为了实现这个功能,建立一个名为Math.asmx的包含下面的简单声明的新文本文件,并把它放到包含该部件的虚拟目录中(注意:它自己进入虚拟目录而不是子目录bin中):

<%@ WebService class="MathService"%>

  这个声明告诉.asmx处理程序使用哪个类检查WebMethods,并且该处理程序自动处理其它的信息。例如,假定虚拟目录叫作”math”并且它包含了Math.asmx,并且bin子目录包含了该部件,那么浏览http://localhost/math/math.asmx将导致.asmx处理程序生成图2所示的文档页面。

  这与.asmx处理程序如何工作有较大的变化。.asmx文件通常只包含通过名字引用Web服务类(如上所示)的WebService声明。因此,在这种情况下,该部件必须已经被编译好、配置到了虚拟目录的bin目录中。.asmx处理程序也提供.asmx文件中源代码的just-in-time(实时)编译。例如,下面的文件(叫作Mathjit.asmx)包含了WebService声明和被引用类的源代码。

<@% WebService class=”MathServiceJit” language=”C#”%>

using System.Web.Services;

public class MathServiceJit
{
   [WebMethod]
   public double Add(double x, double y) {
      return x + y;
   }
   [WebMethod]
   public double Subtract(double x, double y) {
      return x – y;
   }
   public double Multiply(double x, double y) {
      return x * y;
   }
   public double Divide(double x, double y) {
      return x / y;
   }
}

  第一次通过HTTP访问这个文件时,.asmx处理程序编译源代码并把部件配置到正确的位置。注意WebService声明必须提供语言,这样.asmx处理程序才能在运行时选择正确的编译器。这种方法的一个明显的问题是直到你第一次访问该文件时才会发现编译错误。

  图2:MathService文档

  当你使用Visual Studio .NET建立一个新的Web服务项目时,它通常使用”双文件”技术,把源文件与引用它的.asmx文件分开。集成开发环境(IDE)隐藏了这些文件,但是你可以点击”解决方案浏览器”工具条上的Show All Files(显示所有文件),你会发现项目中的每个Web服务类都有两个文件。实际上,Visual Studio .NET并不支持.asmx文件的高亮度提醒或IntelliSense。有了Web项目后,Visual Studio .NET也处理建立虚拟目录并自动把部件编译到虚拟目录的bin目录中。

  在深入分析.asmx处理程序如何工作前,我们先简短讨论一下来自IIS的消息如何分派到用于处理的.asmx处理程序中。当输入的HTTP消息到达80端口时,IIS使用自己的元数据库(metabase)的信息来找出使用哪一个ISAPI DLL来处理这个消息。.NET安装程序把.asmx扩展映射到Aspnet_isapi.dll,如图3所示。

  图3:.asmx 的IIS应用程序映射

  Aspnet_isapi.dll是.NET框架组件提供的一个标准的ISAPI扩充,它简单地把HTTP请求转发到单独的叫作Aspnet_wp.exe的工作进程中。Aspnet_wp.exe寄宿了通用语言运行时和.NET HTTP管道。一旦消息进入.NET HTTP管道,管道就查询配置文件,看应该使用哪一个IhttpHandler类处理给定的扩充。如果你查看Machine.config文件,你会发现它包含了.asm文件的一个httpHandler映射,如下所示:


  
      
        type=”System.Web.Services.Protocols.WebServiceHandlerFactory,
System.Web.Services, Version=1.0.3300.0, Culture=neutral,
PublicKeyToken=b03f5f7f11d50a3a” validate=”false”/>

  因此当某个目标为.asmx文件的消息进入.NET HTTP管道时,该管道调用WebServiceHandlerFactory类来实例化用于处理这个请求的新的WebServiceHandler对象。WebServiceHandler对象打开物理的.asmx文件以决定包含WebMethods的类的名称。

  一旦.asmx处理程序被.NET HTTP管道调用,它就开始处理XML、XSD、SOAP和WSDL进程。.asmx处理程序提供的功能可以分为三个部分:1)消息发送;2)把XML映射到对象;3)自动化WSDL和文档生成。下面讲详细讲解每一部分。

  消息发送

  当HTTP管道调用.asmx处理程序时,它通过查看.asmx文件中的WebService声明来找出使用哪个.NET类来检查。接着它查看输入的HTTP消息的信息以正确地决定调用被引用类地哪个方法。为了调用前面的例子中显示的Add操作,输入的HTTP消息必须有类似下面的信息:

POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: “http://tempuri.org/Add”

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      33
      66
    

  

  在输入的HTTP消息中的确有两部分信息可以用于找出调用类中的哪个方法:SOAPAction头部或者请求的元素名称(例如soap:Body元素内的元素名称)。在这种情况下,任何一个都表明了发送者希望调用的方法的名称。

  默认情况下.asmx处理程序使用SOAPAction头部的值来执行消息发送。因此,.asmx 处理程序查看消息中的SOAPAction头部,接着使用.NET反射(reflection)检查被引用类中的方法。它只考虑使用[WebMethod]标志标记的方法,但是它通过查看每个方法的SOAPAction值来正确地决定调用哪个方法。因为我们没有在自己的类的方法中明确指定SOAPAction的值,.asmx处理程序就假定SOAPAction的值是Web服务的名字空间后面跟上方法名称的组合。因为我们也没有指定名字空间,处理程序就把http://tempuri.org作为默认值。因此Add方法的默认的SOAPAction值是http://tempuri.org/Add。

  你可以使用[WebService]标志作为类的注解来自定义Web服务的名字空间,使用下面所说明的[SoapDocumentMethod]标志作为WebMethods的注解来指定SOAPAction的值:

using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace="http://example.org/math")]
public class MathService
{
   [WebMethod]
   public double Add(double x, double y) {
      return x + y;
   }
   [WebMethod]
   [SoapDocumentMethod(Action="urn:math:subtract")]
   public double Subtract(double x, double y) {
      return x – y;
   }
   …
}

  现在.asmx处理程序认为Add 方法的SOAPAction的值为http://example.org/math/Add(使用默认的启发式),Subtract方法的为urn:math:subtract(因为我们明确地定义了这个值)。例如,下面的HTTP请求消息调用Subtract操作:

POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: “urn:math:subtract”

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      33
      66
    

  

  如果.asmx处理程序无法查找到匹配输入的HTTP消息的SOAPAction,它简单地抛出一个异常(后面将解释异常如何处理)。如果你不愿意依赖SOAPAction头部来进行方法调度,你可以指示.asmx处理程序通过使用[SoapDocumentService]标志的RoutingStyle属性作为类的注解来使用请求元素地名称。如果你这样做了,你大概也会通过把SOAPAction的值设置为空字符串来表明WebMethods不需要SOAPAction值:

using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace="http://example.org/math")]
[SoapDocumentService(
  RoutingStyle=SoapServiceRoutingStyle.RequestElement)]
public class MathService
{
   [WebMethod]
   [SoapDocumentMethod(Action="")]
   public double Add(double x, double y) {
      return x + y;
   }
   [WebMethod]
   [SoapDocumentMethod(Action="")]
   public double Subtract(double x, double y) {
      return x – y;
   }
    …
}

  在这种情况下,处理程序不会查看SOAPAction值–它使用请求元素的名称作为代替。例如,它认为Add方法的请求元素的名称为Add(来自http://example.org/math名字空间),下面演示了这种HTTP请求消息:

POST /math/math.asmx HTTP/1.1
Host: localhost
Content-Type: text/xml; charset=utf-8
Content-Length: length
SOAPAction: “”

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      33
      66
    

  

  因此,.asmx处理程序接收到一个输入的HTTP请求时,第一件重要的事情是找出怎样把这个消息发送到对应的WebMethod。但是在能够实际调用该方法前,它需要把输入的XML映射到.NET对象。

  把XML映射到对象

  一旦WebMehod处理程序找出了将调用哪个方法,接着它就需要把XML消息转换为可以提供给方法调用的.NET对象。在消息发送后,处理程序通过反射检查该类找出如何处理输入的XML消息以达到这个目的。System.Xml.Serialization名字空间中的XmlSerializer类执行XML和对象之间的自动映射。

  XmlSerializer使任何公共的.NET类型映射到XML Schema类型成为可能,并且有了类似的映射,它可以在.NET对象和XML实例文档之间自动映射(图4所示)。目前XmlSerializer被限制为XML Schema所支持的模型,因此不能处理目前现代的对象模型(例如复杂的非树型对象图表、两重指针等等)的所有复杂性。然而,XmlSerializer可以处理开发者趋向使用的大多数复杂的类型。

  对于上面的例子中所示的Add方法,XmlSerializer会把x和y元素映射到.NET双精度型值,那么调用Add时它们就可以使用了。Add方法给调用者返回一个双精度型值,接着需要串行化该值返回为SOAP响应中的一个XML元素。

  图4:把XML映射到对象

  XmlSerializer也能自动处理复杂的类型(除了上面描述的限制)。例如,下面的WebMethod计算两个Point(点)结构体之间的距离。

using System;
using System.Web.Services;

public class Point {
  public double x;
  public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
  [WebMethod]
  public double Distance(Point orig, Point dest) {
    return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
                     Math.Pow(orig.y-dest.y, 2));
  }
}

这种操作的SOAP请求消息将包含一个Distance元素,该元素包含两个子元素,一个叫作orig,另一个叫dest,它们每个都包含两个子元素x和y,如下所示:

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      
        0
        0
      

      
        3
        4
      

    

  

  这种情况下的SOAP响应消息将包含一个DistanceResponse元素,该元素包含一个双精度类型的DistanceResult元素:

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
          xmlns=”urn:geometry”>
      5
    

  

  默认的XML映射使用方法的名称作为请求元素的名称,参数的名称作为它的子元素的名称。每个参数的结构依赖于类型的结构。公共字段和属性的名称简单地映射到子元素(这种情况下是Point中的x和y)。默认情况下响应元素的名称是请求元素的名称后面加上”Response”。响应元素也包含一个子元素,名称为请求元素的名称加上”Result”。

  你可以使用一系列内建的映射标志从标准的XML映射中解放出来。例如,你可用使用[XmlType]标志自定义类型的名称和名字空间。你可以使用[XmlElement]和[XmlAttribute]标志来控制参数或类成员如何分别映射到元素或属性。你可以使用[SoapDocumentMethod]标志来控制方法自身如何映射到请求/响应消息中的元素名称。例如,你可以看一看下面版本的Distance示例的多种标志:

using System;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Serialization;

public class Point {
  [XmlAttribute]
  public double x;
  [XmlAttribute]
  public double y;
}
[WebService(Namespace="urn:geometry")]
public class Geometry {
  [WebMethod]
  [SoapDocumentMethod(RequestElementName="CalcDistance",
     ResponseElementName="CalculatedDistance")]
  [return: XmlElement("result")]
  public double Distance(
    [XmlElement("o")]Point orig, [XmlElement("d")]Point dest) {
    return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +
                     Math.Pow(orig.y-dest.y, 2));
  }
}

  这个版本的Distance要求输入的SOAP消息格式如下:

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      
      
    

  

  它生成的SOAP响应消息的格式如下:

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      5
    

  

  该.asmx处理程序使用SOAP document/literal样式来实现和描述上面所示的默认映射。这意味着该WSDL定义将包含描述SOAP消息中所使用的请求和响应元素的字面上的XML schema定义(例如,没有使用SOAP编码规则)。

  该.asmx处理程序也可以使用SOAP rpc/encoded样式。这意味着SOAP主体(Body)包含一个RPC调用的XML表现,并且参数都使用SOAP编码规则串行化了(例如,不需要XML Schema)。为了达到这个目标,使用[SoapRpcService]和[SoapRpcMethod]代替[SoapDocumentService]和[SoapDocumentMethod]标志。如果你要了解这些样式之间的差别,请参阅Understanding SOAP。

  从上面的信息中你可以发现,完全地自定义如何把给定的方法映射到SOAP消息是可能的。XmlSerializer提供了强大的串行化引擎以及许多我们在此文中没有讨论的特性。如果你要了解XmlSerializer如何工作的详细信息,请查阅Moving to .net and Web Services。

  作为处理参数的并行化的补充,.asmx处理程序也能够并行化/串行化SOAP头部。SOAP头部的处理方法与参数不同,因为典型情况下它们被认为是范围之外的信息,没有直接关联到某个特定的方法。由于这个原因,典型的头部处理是由监听层完成的,使WebMethod根本不用进行头部处理。

  但是,如果你希望在WebMethod中处理头部信息,你必须提供一个演示自SoapHeader(它描述了头部的XML Schema类型)的.NET类。接着你定义该类型的一个成员变量作为头部实例的位置标志符。最后,你给每个需要访问该头部的WebMethod作注解,指定你需要处理的字段的名称。

  例如,看一看下面的包含用于身份验证目的的UsernameToken头部的SOAP请求:

  xmlns:soap=”http://schemas.xmlsoap.org/soap/envelope/”
>
  
    
      Mary
       yraM    

  

  
    
       …

  为了使.asmx处理程序能够并行化该头部,首先你必须定义一个描述隐含的XML Schema类型的.NET类(注意:如果你已经有了该头部的XML Schema,那么可以使用xsd.exe /c生成这个类)。在这种情况下,相应的类如下所示:

[XmlType(Namespace="http://example.org/security")]
[XmlRoot(Namespace="http://example.org/security")]
public class UsernameToken : SoapHeader {
   public string username;
   public string password;
}

  接着,你必须在WebMethod类中简单地定义一个成员变量来保持该头部类的一个实例,并使用[SoapHeader]标志来注解该WebMethod,如下所示:

using System;
using System.Web.Services;
using System.Web.Services.Protocols;

[WebService(Namespace="urn:geometry")]
public class Geometry {

  public UsernameToken Token;

  [WebMethod]
  [SoapHeader("Token")]
  public double Distance(Point orig, Point dest) {

    if (!Token.username.Equals(Reverse(Token.password)))
       throw new Exception(“access denied”);

    return Math.Sqrt(Math.Pow(orig.x-dest.x, 2) +Math.Pow(orig.y-dest.y, 2));
  }
}

  接着,在WebMethod中你可以访问该头部提供的Token字段并提取信息。你也可以使用相同的技术把该头部送回给客户端–你只需要在[SoapHeader]标志声明中简单地指定头部的方向。需要了解在WebMethod框架组件中处理SOAP头部的更多信息,请查阅Digging into SOAP Headers with the .net Framework。

  .asmx处理程序也提供了.NET异常的自动的串行化。任何被.asmx捕捉到的没有处理的异常都自动地串行化进入响应中的SOAP Fault元素。例如,在前面的例子中,如果用户名与与密码不匹配,代码将抛出一个.NET异常。接着.asmx处理程序捕捉到这个异常并串行化到下面所示的SOAP响应中:


  
    
      
soap:Server
    
      
Server was unable to process request. –> access denied

      
    

  

  如果你需要更多地SOAP Fault元素控制权,可以明确地抛出一个SoapException对象,指定所有的SOAP Fault元素细节,例如faultcode、faulstring、faultactor和detail元素。你可以参阅Using SOAP Faults获得更多信息。

  如上所示,指出WebMethod如何工作必须了解下层串行化引擎和它的多种选项。串行化引擎的优点是它隐藏了所有的通常在编写自定义处理程序中需要的下层XML API代码。但是,尽管大多数开发者发现这是有正面意义的,有一些开发者认为它是一个缺陷,因为它们仍然希望在WebMethod实现中手动包装SOAP消息。要了解如何实现这种混合的途径的详细信息,请参阅Accessing Raw SOAP Messages in ASP.NET Web Services。

  自动生成WSDL

  一旦你已经编写并配置了一个WebMethod,客户端为了成功地与它通讯,需要了解正确地SOAP消息是什么样子的。提供Web服务描述的标准的途径是WSDL(或嵌入式XSD定义)。为了帮助适应这种情况,.asmx处理程序自动生成人类可以阅读的文档页面和准确反映WebMethod接口的WSDL定义。如果你给WebMethods应用了一系列的映射标志,它们都会反映在生成的文档中。

  如果你浏览.asmx文件,你会看到类似图2所示的可以阅读的文档页面。该文档页面是由一个叫作DefaultWsdlHelpGenerator.aspx的.aspx页面(位置在C:\windows\Microsoft.NET\Framework\ v1.0.3705\config)生成的。如果打开这个文件,你可以发现这仅仅是一个使用.NET反射生成文档的标准的ASP.NET页面。这个特性使你的文档一直与代码同步。你可以简单地修改这个文件来自定义生成的文档。

  你可以通过在Web.config文件中指定一个不同的文档文件来绕过在虚拟目录基础上的文档生成:


  
    
      
    

    …

  如果客户端提出的.asmx端点的GET请求在请求字符串中有”?wsdl”,那么.asmx处理程序会生成WSDL定义来代替可以阅读的文档。客户端可以使用这个WSDL定义来生成自动了解如何与Web服务通讯的代理类(例如在.NET中使用Wsdl.exe)。

  为了自定义WSDL生成进程,你可以编写一个SoapExtensionReflector类并在Web.config文件中向WebMethods框架组件注册。接着,当.asmx处理程序生成WSDL定义时,它将调用你的反射类,给了你自定义最终提供给客户端的定义的机会。如果你要了解更多的编写SoapExtensionReflector类的方法,请查看SoapExtensionReflectors in ASP.NET Web Services。

  你可以使用两种不同的技术来绕过WSDL生成进程。第一种是你可以在虚拟目录种为客户端的访问提供静态的WSDL文档并在Web.config文件中删除文档生成部分,如下所示:


  
    
               
      
      …

  另一种稍微自动化的技术是使用[WebServicesBinding]标志来指定实现WebMethod类的虚拟目录中的静态WSDL文档的位置。你也必须使用[SoapDocumentMethod]标志指定帮定到每个WebMethod实现的WSDL的名称。有了这些后,自动化的WSDL生成进程将导入你的静态WSDL文件并在它周围包装一个新的服务描述。如果你要了解这种技术的更多信息,请查阅Place XML Message Design Ahead of Schema Planning to Improve Web Service Interoperability。

  目前WSDL极难手动编写,因为没有很多的可用的WSDL编辑器。因此,自动的文档/WSDL生成是WebMethods框架组件中有价值的一部分,很多开发者将很长时间依赖它。

  结论

  ASP.NET的WebMethods框架组件提供了一条高效率建立Web服务的途径。WebMethods把传统.NET方法为支持HTTP、XML、XML Schema、SOAP和WSDL暴露为Web服务操作成为可能。WebMethods(.asmx)处理程序自动找出怎样把输入的SOAP消息分派给适当的方法,这时它自动把输入的XML元素串行化成相应的.NET对象。为了简化与客户端的集成,.asmx处理程序也提供了生成人类可读(HTML)和计算机可读(WSDL)文档的自动支持。

  尽管WebMethods框架组件与自定义IHttpHandlers相比稍微有点限制,但是它也提供了强大的可扩展性模型,就是我们所知道的SOAP扩展框架组件。SOAP扩展允许你根据需要引入附加的功能。例如,微软为.NET发布了Web Services Enhancements 1.0(WSE),它仅仅提供了一个SoapExtension类,该类为WebMethods框架组件引入了对几种GXA规格的支持。如果你需要了解编写SOAP扩展的更多信息,请参阅Fun with SOAP Extensions。

  原作者:Aaron Skonnard

  原标题:How ASP.NET Web Services Work

2004年04月26日

 摘要:讨论如何在?ASP.NET?1.1?中实现会话状态功能,以及如何在被管理的?Web?应用程序中优化会话状态管理。(本文包含一些指向英文站点的链接。)

目录
简介
ASP.NET?会话状态概述
同步访问会话状态
比较状态提供程序
状态序列化和反序列化
会话的生命周期
Cookieless?会话
小结

简介
在?Web?应用程序这样的无状态环境中,了解会话状态的概念并没有实际的意义。尽管如此,有效的状态管理对于大多数?Web?应用程序来说都是一个必备的功能。Microsoft??ASP.NET?以及许多其他服务器端编程环境都提供了一个抽象层,允许应用程序基于每个用户和每个应用程序存储持久性数据。

需要特别注意的是,Web?应用程序的会话状态是应用程序在不同的请求中缓存和检索的数据。会话表示用户在与该站点连接期间发送的所有请求,会话状态是用户在会话期间生成和使用的持久性数据的集合。每个会话的状态都彼此独立,而且在用户会话结束时就不复存在了。

会话状态与构成?HTTP?协议和规范的任何逻辑实体都没有对应关系。会话是由服务器端开发环境(例如传统的?ASP?和?ASP.NET)构建的抽象层。ASP.NET?展示会话状态的方式以及会话状态的内部实现方式都取决于平台的基础结构。因此,传统的?ASP?和?ASP.NET?以完全不同的方式来实现会话状态,预计在下一版的?ASP.NET?中会有进一步的改进和增强。?

本文讨论如何在?ASP.NET?1.1?中实现会话状态,以及如何在被管理的?Web?应用程序中优化会话状态管理。?

ASP.NET?会话状态概述
会话状态并不是?HTTP?基础结构的一部分。也就是说,应该有一个结构组件将会话状态与每个传入请求绑定在一起。运行时环境(传统的?ASP?或?ASP.NET)能够接受?Session?之类的关键字,并使用它指示服务器上存储的数据块。要成功解析?Session?对象的调用,运行时环境必须将会话状态添加到正在处理的请求的调用上下文中。完成此操作的方式因平台而异,但它是有状态?Web?应用程序的基础操作。

在传统的?ASP?中,会话状态是作为?asp.dll?库中包含自由线程?COM?对象来实现的。(您对此很好奇吗?其实该对象的?CLSID?是?D97A6DA0-A865-11cf-83AF-00A0C90C2BD8。)此对象存储以名称/值对集合的方式组织的数据。“名称”占位符表示用来检索信息的关键字,而“值”占位符表示会话状态中存储的内容。名称/值对按照会话?ID?进行分组,这样,每个用户看到的只是他/她自己创建的名称/值对。?

在?ASP.NET?中,会话状态的编程接口与传统的?ASP?几乎是相同的。但它们的基础实现是完全不同的,前者比后者更具有灵活性、可扩展性和更强的编程功能。深入研究?ASP.NET?会话状态之前,让我们简单回顾一下?ASP.NET?会话基础结构的某些结构功能。

在?ASP.NET?中,任何传入?HTTP?请求都要通过?HTTP?模块管道进行传输。每个模块都可以筛选并修改请求所携带的大量信息。与每个请求关联的信息叫做“调用上下文”,编程中用?HttpContext?对象来表示。我们不应将请求的上下文视为状态信息的另一个容器,虽然它提供的?Items?集合只是一个数据容器。HttpContext?对象不同于所有其他状态对象(例如,Session、Application?和?Cache),因为它的有限生命周期超出了处理请求所需的时间。当请求通过一系列注册的?HTTP?模块后,其?HttpContext?对象将包含状态对象的引用。当最终可以处理请求时,关联的调用上下文将绑定到特定会话?(Session)?和全局状态对象(Application?和?Cache)。?

负责设置每个用户的会话状态的?HTTP?模块为?SessionStateModule。该模块的结构是根据?IHttpModule?接口设计的,它为?ASP.NET?应用程序提供大量与会话状态有关的服务。包括生成会话?ID、Cookieless?会话管理、从外部状态提供程序中检索会话数据以及将数据绑定到请求的调用上下文。?

HTTP?模块并不在内部存储会话数据。会话状态始终保存在名为“状态提供程序”的外部组件中。状态提供程序完全封装会话状态数据,并通过?IStateClientManager?接口的方法与其他部分进行通信。会话状态?HTTP?模块调用该接口上的方法来读取并保存会话状态。ASP.NET?1.1?支持三种不同的状态提供程序,如表?1?所示。

表?1:状态客户端提供程序

提供程序?说明?
InProc?会话值在?ASP.NET?辅助进程(Microsoft??Windows?Server??2003?中的?aspnet_wp.exe?或?w3wp.exe)的内存中保持为活动对象。这是默认选项。?
StateServer?会话值被序列化并存储在单独进程?(aspnet_state.exe)?的内存中。该进程还可以在其他计算机上运行。??
SQLServer?会话值被序列化并存储在?Microsoft??SQL?Server??表中。SQL?Server?的实例可以在本地运行,也可以远程运行。??

会话状态?HTTP?模块将从?web.config?文件的??部分读取当前选定的状态提供程序。?


上述代码实际上访问的是?HTTP?模块创建的会话值在本地内存中的副本,从特定状态提供程序(参见图?1)中读取数据。如果其他页面也试图同步访问该会话状态,又会如何呢?这种情况下,当前的请求可能会停止处理不一致的数据或过时的数据。为了避免这种情况,会话状态模块将实现一个读取器/写入器锁定机制,并对状态值的访问进行排队。对会话状态具有写入权限的页面将保留该会话的写入器锁定,直到请求终止。

通过将?@Page?指令的?EnableSessionState?属性设置为?true,页面可以请求会话状态的写入权限。(这是默认设置)。但是,页面还可以拥有会话状态的只读权限,例如,当?EnableSessionState?属性被设置为?ReadOnly?时。在这种情况下,模块将保留该会话的读取器锁定,直到该页面的请求结束。结果将发生并发读取。?

如果页面请求设置一个读取器锁定,同一会话中同时处理的其他请求将无法更新会话状态,但是至少可以进行读取。也就是说,如果当前正在处理会话的只读请求,那么等候的只读请求要比需要完全访问权限的请求具有更高的优先权。如果页面请求为会话状态设置一个写入器锁定,那么所有其他页面都将被阻止,无论它们是否要读取或写入内容。例如,如果同时有两个框架试图在?Session?中写入内容,一个框架必须等到另一个框架完成后才能写入。

比较状态提供程序
默认情况下,ASP.NET?应用程序将会话状态存储在辅助进程的内存中,特别是?Cache?对象的专用槽中。选中?InProc?模式时,会话状态将存储在?Cache?对象内的槽中。此槽被标记为专用槽,无法通过编程方式进行访问。换句话说,如果枚举?ASP.NET?数据缓存中的所有项目,将不会返回类似于给定会话状态的任何对象。Cache?对象提供两类槽:专用槽和公用槽。编程人员可以添加和处理公用槽,但专用槽只能由系统(特别是?system.web?部件中定义的类)专用。?

每个活动会话的状态都占用缓存中的一个专用槽。槽的名称根据会话?ID?进行命名,其值是名为?SessionStateItem?的内部未声明类的一个实例。InProc?状态提供程序获取会话?ID?并在缓存中检索对应的元素。然后将?SessionStateItem?对象的内容输入?HttpSessionState?词典对象,并由应用程序通过?Session?属性进行访问。请注意,ASP.NET?1.0?中存在一个错误,使?Cache?对象的专用槽可以通过编程方式进行枚举。如果您在?ASP.NET?1.0?下运行以下代码,则能够枚举与每个当前活动会话状态中包含的对象对应的项目。?

foreach(DictionaryEntry?elem?in?Cache)
{
???Response.Write(elem.Key?+?”:?”?+?elem.Value.ToString());
}

此错误已经在?ASP.NET?1.1?中得到解决,当您枚举缓存的内容时,将不再列出任何系统槽。

到目前为止,InProc?可能是最快的访问选项。但请记住,会话中存储的数据越多,Web?服务器所消耗的内存就越多,这样会潜在地增加性能降低的风险。如果您计划使用任何进程外解决方案,应该认真考虑一下序列化和反序列化可能带来的影响。进程外解决方案使用?Windows?NT?服务?(aspnet_state.exe)?或?SQL?Server?表来存储会话值。因此,会话状态保留在?ASP.NET?辅助进程之外,并且需要使用额外的代码层,在会话状态和实际的存储介质之间进行序列化和反序列化操作。只要处理请求就会发生此操作,而且随后必须对其进行最高程度的优化。

因为需要将会话数据从外部储备库复制到本地会话词典中,所以请求导致性能下降了?15%(进程外)到?25%?(SQL?Server)。请注意,虽然这只是一种粗略的估计,但它应该接近于最低程度的影响,最高程度的影响将远高于此。实际上,这种估计并没有完全考虑到会话状态中实际保存的类型的复杂程度。?

在进程外存储方案中,会话状态存活的时间较长,使应用程序的功能更强大,因为它可以防止?Microsoft??Internet?信息服务?(IIS)?和?ASP.NET?失败。通过将会话状态与应用程序相分离,您还可以更容易地将现有应用程序扩展到?Web?Farm?和?Web?Garden?体系结构中。另外,会话状态存储在外部进程中,从根本上消除了由于进程循环而导致的周期性数据丢失的风险。

下面介绍如何使用?Windows?NT?服务。正如上文所述,NT?服务是一个名为?aspnet_state.exe?的进程,通常位于?C:\WINNT\Microsoft.NET\Framework\v1.1.4322?文件夹中。

实际目录取决于您实际运行的?Microsoft??.net?Framework?版本。使用状态服务器之前,应确保该服务就绪并正运行在用作会话存储设备的本地或远程计算机上。状态服务是?ASP.NET?的组成部分并与之一起安装,因此您无需运行其他安装程序。默认情况下,状态服务并没有运行,需要手动启动。ASP.NET?应用程序将在加载状态服务器之后立即尝试与之建立连接。因此,该服务必须准备就绪且正在运行,否则将引发?HTTP?异常。下图显示了该服务的属性对话框。

图?2:ASP.NET?状态服务器的属性对话框

ASP.NET?应用程序需要指定会话状态服务所在的计算机的?TCP/IP?地址。必须将以下设置输入该应用程序的?web.config?文件中。?


????
????????????????????mode=”StateServer”?
????????????stateConnectionString=”tcpip=expoware:42424″?/>
????

stateConnectionString?特性包含计算机的?IP?地址以及用来进行数据交换的端口。默认的计算机地址为?127.0.0.1(本地主机),默认端口为?42424。您也可以按名称指示计算机。对于代码来说,使用本地或远程计算机是完全透明的。请注意,不能在该名称中使用非?ASCII?字符,并且端口号是强制的。?

如果您使用进程外会话存储,会话状态将仍然存在并且可供将来使用,无论?ASP.NET?辅助进程出现何种情况。如果该服务被中断,数据将被保留下来,并且在该服务恢复时自动进行检索。但是,如果状态提供程序服务停止或失败,数据将丢失。如果您希望应用程序具有强大的功能,请使用?SQLServer?模式,而不要使用?StateServer?模式。


????
????????????????????mode=”SQLServer”?
????????????sqlConnectionString=”server=127.0.0.1;uid=pwd= “?/>
????

您可以通过?sqlConnectionString?特性指定连接字符串。请注意,特性字符串必须包含用户?ID、密码和服务器名称。它不能包含?Database?和?Initial?Catalog?之类的标记,因为此信息默认为固定名称。用户?ID?和密码可以替换为集成的安全设置。

如何创建数据库?ASP.NET?提供两对脚本来配置数据库环境。第一对脚本名为?InstallSqlState.sql?和?UninstallSqlState.sql,与会话状态?NT?服务位于同一个文件夹中。它们创建名为?ASPState?的数据库和几个存储的过程。但是,数据存储在?SQL?Server?临时存储区域?TempDB?数据库中。这意味着,如果重新启动?SQL?Server?计算机,会话数据将丢失。

要解决这一局限性,请使用第二对脚本。第二对脚本名为?InstallPersistSqlState.sql?和?UninstallPersistSqlState.sql。在这种情况下,将创建?ASPState?数据库,但是会在同一个数据库中创建数据表,而且这些数据表同样是持久的。为会话安装?SQL?Server?支持时,还将创建一个作业,以删除会话状态数据库中过期的会话。该作业名为?ASPState_Job_DeleteExpiredSessions?并且一直运行。请注意,要使该作业正常进行,需要运行?SQLServerAgent?服务。

无论您选择哪种模式,为会话状态操作进行编码的方式都不会改变。您可以始终针对?Session?属性进行工作并像平常一样读取和写入值。所有行为上的差异都是在较低的抽象层上处理的。状态序列化或许是会话模式之间的最重要差异。

状态序列化和反序列化
使用进程内模式时,对象作为各自类的活动实例存储在会话状态中。如果未发生真正的序列化和反序列化,则表示您实际上可以在?Session?中存储您创建的任何对象(包括无法序列化的对象和?COM?对象),并且访问它们的开销也不会太高。如果您选择进程外状态提供程序,又是另外一种情况。?

在进程外体系结构中,会话值将从本地存储介质(外部?AppDomain?数据库)复制到处理请求的?AppDomain?的内存中。需要使用序列化/反序列化图层完成该任务,并表示进程外状态提供程序的某项主要成本。这种情况对代码产生的主要影响是只能在会话词典中存储可序列化的对象。?

根据所涉及的数据类型,ASP.NET?使用两种方法对数据进行序列化和反序列化。对于基本类型,ASP.NET?使用经过优化的内部序列化程序;对于其他类型(包括对象和用户定义的类),ASP.NET?使用?.net?二进制格式化程序。基本类型包括字符串、日期时间、布尔值、字节、字符以及所有的数字类型。对于这些类型,使用量身制作的序列化程序要比使用默认的常用?.net?二进制格式化程序更快。?

经过优化的序列化程序没有公开发布,也没有以文档形式提供。它仅仅是二进制读取器/写入器,并且使用简单但有效的存储架构。该序列化程序使用?BinaryWriter?类写入一个字节表示类型,然后写入一个字节表示该类型对应的值。读取序列化的字节时,该类首先提取一个字节,检测要读取的数据类型,然后对?BinaryReader?类调用特定类型的?ReadXxx?方法。?

请注意,布尔值和数字类型的大小是众所周知的,但对字符串并非如此。在基础数据流上,字符串始终带有一个固定长度的前缀(一次编写?7?位整数代码),读取器根据这一事实来确定字符串的正确大小。而日期值是通过只写入构成日期的标记总数来保存的。因此,要对会话执行序列化操作,日期应为?Int64?类型。

只要将包含的类标记为可序列化的类,便可以使用?BinaryFormatter?类对更复杂的对象(以及自定义对象)执行序列化操作。所有非基本类型都采用相同的类型?ID?进行标识并与基本类型存储在同一个数据流中。总之,序列化操作会导致性能下降?15%?至?25%。但请注意,这是基于假定使用基本类型所进行的粗略估计。使用的类型越复杂,开销越大。?

如果不大量使用基本类型,很难实现有效的会话数据存储。因此,至少在理论上,使用三个会话槽保存对象的三个不同的字符串属性要比对整个对象进行序列化好。但是,如果要序列化的对象包含?100?个属性,那该怎么办呢?是要使用?100?个槽,还是只使用一个槽?在许多情况下,更好的方法是将复杂的类型转换为多个简单的类型。这种方法基于类型转换器。“类型转换器”是一种轻便的序列化程序,它以字符串集合的形式返回类型的关键属性。类型转换器是使用特性与基类绑定在一起的外部类。由类型编写者决定保存哪些属性以及如何保存。类型转换器对于?ViewState?存储也有帮助,它代表的是比二进制格式化程序更有效的会话存储方法。?

会话的生命周期
关于?ASP.NET?会话管理,重要的一点是,仅当将第一个项目添加到内存词典中时,会话状态对象的生命周期才开始。仅在执行如下代码片断后,才可以认为?ASP.NET?会话开始。

Session["MySlot"]?=?”Some?data”;

Session?词典通常包含?Object?类型,要向后读取数据,需要将返回的值转换为更具体的类型。?

string?data?=?(string)?Session["MySlot"];

当页面将数据保存到?Session?中时,会将值加载到?HttpSessionState?类包含的特制的词典类中。完成当前处理的请求时,会将词典的内容加载到状态提供程序中。如果由于未通过编程方式将数据放入词典而导致会话状态为空,则不会将数据序列化到存储介质中,而且更重要的是,不会在?ASP.NET?Cache、SQL?Server?或?NT?状态服务中创建槽来跟踪当前会话。这是出于性能方面的原因,但会对处理会话?ID?的方式产生重要影响:将为每个请求生成一个新的会话?ID,直到将某些数据存储到会话词典中。?

需要将会话状态与正在处理的请求连接时,HTTP?模块会检索会话?ID(如果它不是启动请求),并在配置的状态提供程序中寻找它。如果没有返回数据,HTTP?模块将为请求生成一个新的会话?ID。这可以很容易地通过以下页面进行测试:

<%@?Page?Language="C#"?Trace="true"?%>



无论何时单击该按钮并返回页面,都将生成新的会话?ID,同时记录跟踪信息。?

图?3:在没有将数据存储到会话词典中的应用程序中,为每个请求生成一个新的会话?ID。

Session_OnStart?事件的情况如何呢?也会为每个请求引发该事件吗?如果应用程序定义?Session_OnStart?处理程序,则会始终保存会话状态,即使会话状态为空。因此,对于第一个请求之后的所有请求来说,会话?ID?始终为常量。仅在确实必要时,才使用?Session_OnStart?处理程序。?

如果会话超时或被放弃,下次访问无状态应用程序时,其会话?ID?不会发生改变。经过设计后,即使会话状态过期,会话?ID?也能持续到浏览器会话结束。也就是说,只要浏览器实例相同,就始终使用同一个会话?ID?表示多个会话。

Session_OnEnd?事件标志着会话的结束,并用于执行终止该会话所需的所有清除代码。但请注意,只有?InProc?模式支持该事件,也就是说,只有将会话数据存储在?ASP.NET?辅助进程中时才支持该事件。对于要引发的?Session_OnEnd?事件来说,必须首先存在会话状态,这意味着必须在该会话状态中存储一些数据,并且必须至少完成一个请求。?

在?InProc?模式下,作为项目添加到缓存中的会话状态被赋予一个可变过期时间策略。可变过期时间表示如果某个项目在一定时间内没有使用,将被删除。在此期间处理的任何请求的过期时间都将被重置。会话状态项目的时间间隔被设置为会话超时。用来重置会话状态过期时间的技术非常简单和直观:会话?HTTP?模块只读取?ASP.NET?Cache?中存储的会话状态项目。如果知道?ASP.NET?Cache?对象的内部结构,该模块将进行计算以重新设置可变过期时间。因此,当缓存项目过期时,会话已超时。?

过期的项目将自动从缓存中删除。状态会话模块作为此项目的过期时间策略的一部分,也代表了一个删除回调函数。缓存将自动调用删除函数,删除函数然后将引发?Session_OnEnd?事件。如果应用程序通过进程外组件来执行会话管理,则永远不会引发结束事件。

Cookieless?会话
每个活动?ASP.NET?会话都是使用仅由?URL?允许的字符组成的?120?位字符串标识的。会话?ID?是使用随机数生成器?(RNG)?加密提供程序生成的。该服务提供程序返回一个包含?15?个随机生成数的序列(15?字节?x?8?位?=?120?位)。随机数数组然后被映射到有效的?URL?字符并以字符串形式返回。?

会话?ID?字符串被发送到浏览器,然后通过以下两种方式之一返回服务器应用程序:使用?Cookie(就像在传统?ASP?中一样)或经过修改的?URL。默认情况下,会话状态模块将在客户端创建?HTTP?Cookie,但是可以使用嵌入会话?ID?字符串的修改后的?URL(特别是对于不支持?Cookie?的浏览器)。采用哪种方法取决于应用程序的?web.config?文件中所存储的配置设置。要配置会话设置,可以使用??区段和?Cookieless?特性。

默认情况下,Cookieless?特性为?false,表示使用了?Cookie。实际上,Cookie?只是?Web?页放在客户端硬盘上的一个文本文件。在?ASP.NET?中,Cookie?由?HttpCookie?类的一个实例来表示。通常,Cookie?包含名称、值集合和过期时间。Cookieless?特性被设置为?false?时,会话状态模块实际上将创建一个名为?ASP.NET_SessionId?的?Cookie?并将会话?ID?存储在其中。下面的伪代码显示了创建?Cookie?的过程:

HttpCookie?sessionCookie;
sessionCookie?=?new?HttpCookie(“ASP.NET_SessionId”,?sessionID);
sessionCookie.Path?=?”/”;

会话?Cookie?的过期时间很短,在每个请求成功后更新过期时间。Cookie?的?Expires?属性表示?Cookie?在客户端的过期时间。如果未显式设置会话?Cookie,Expires?属性将默认为?DateTime.MinValue,即?.net?Framework?允许的最小时间单位。?

要禁用会话?Cookie,请在配置文件中将?Cookieless?特性设置为?true,如下所示:


????
????????
????

此时,假设您请求以下?URL?处的页面:

http://www.contoso.com/sample.aspx

浏览器地址栏中实际显示的内容会有所不同,现在包含会话?ID,如下所示:

http://www.contoso.com/(5ylg0455mrvws1uz5mmaau45)/sample.aspx

实例化会话状态?HTTP?模块时,该模块将检查?Cookieless?特性的值。如果为?true,则将请求重定向?(HTTP?302)?到经过修改的包含会话?ID(紧跟在页面名称前)的虚拟?URL。再次处理请求时,请求中会包含该会话?ID。如果请求启动新的会话,HTTP?模块将生成新的会话?ID,然后重定向该请求。如果回传请求,则会话?ID?已经存在,因为回传使用相对?URL。?

使用?Cookieless?会话的缺点是,如果调用绝对?URL,将丢失会话状态。使用?Cookie?时,您可以清除地址栏,转至其他应用程序,然后返回上一个应用程序并检索相同的会话值。如果在禁用会话?Cookie?时执行此操作,将丢失会话数据。例如,以下代码将打断该会话:

Click

如果需要使用绝对?URL,请通过一些小技巧手动将会话?ID?添加到?URL?中。您可以对?HttpResponse?类调用?ApplyAppPathModifier?方法。?

????href=<%?=Response.ApplyAppPathModifier("/code/page.aspx")%>?>Click

ApplyAppPathModifier?方法将使用表示?URL?的字符串,并返回嵌入会话信息的绝对?URL。例如,需要从?HTTP?页面重定向到?HTTPS?页面时,此技巧特别有用。

小结
会话状态最初由传统的?ASP?引入,它是基于词典的?API,使开发人员能够存储会话期间的自定义数据。在?ASP.NET?中,会话状态支持以下两种主要功能:Cookieless?会话?ID?存储和传输,以及会话数据实际存储的状态提供程序。为实现这两种新功能,ASP.NET?利用?HTTP?模块控制会话状态与正在处理的请求上下文之间的绑定。?

在传统的?ASP?中,使用会话状态就是指使用?Cookie。在?ASP.NET?中已不再如此,因为可以使用?Cookieless?架构。借助?HTTP?模块的力量,可以分解请求的?URL?以使其包含会话?ID,然后将其重定向。接下来,HTTP?模块会从该?URL?中提取会话?ID?并使用它检索任何存储的状态。?

会话的物理状态可以存储在三个位置:进程内内存、进程外内存和?SQL?Server?表。数据必须经过序列化/反序列化处理,才能供应用程序使用。HTTP?模块会在请求开始时将会话值从提供程序复制到应用程序的内存中。请求完成后,修改后的状态将返回提供程序。这种数据通信会对性能产生不同程度的不利影响,但是会大大增强可靠性和稳定性,也使对?Web?Farm?和?Web?Garden?体系结构的支持更容易实现。

作者简介
Dino?Esposito?是一位来自意大利罗马的培训教师和顾问。作为?Wintellect?小组的成员,Dino?专门研究?ASP.NET?和?ADO.NET,主要在欧洲和美国从事教学和咨询工作。此外,Dino?还负责管理?Wintellect?的?ADO.NET?课件,并为?MSDN?Magazine?的“Cutting?Edge”专栏撰写文章。

2004年04月24日

窗体对话框组件与微软视窗操作系统中的对话框是一样的;也就是说,PrintDialog 组件是“打印”对话框,OpenFileDialog 组件是 “打开文件”对话框,依此类推。

与以往的 Microsoft Visual Basic 6.0 等 Windows 程序设计语言相似,.NET 框架提供了 Windows 用户耳熟能详的对话框。对话框的具体用途(如 Printdialog 可用于文件打印等)通常是多种多样的。故而在 .net 框架提供的基础类中不包含用于文件打印、颜色选择等具体操作的代码,而你却可以根据应用程序的需要灵活地实现它们。因此,在 .net 框架下,你不但可以直接应用标准对话框,而且能根据用户的选择作出不同的响应。本文提供的代码其用途就在于此。

注意,关于各种对话框的属性、方法和事件的完整描述,可以在相应类的 Members 页面中找到。比如要查看 OpenFileDialog 组件的某一方法,就可以在文档索引的“OpenFileDialog class, all members”栏目中找到相关的主题。

OpenFileDialog 组件

OpenFileDialog 对话框使得用户能够通过浏览本地或者远程的文件系统以打开所选文件。它将返回文件路径和所选的文件名。

OpenFileDialog 组件和 SaveFileDialog 组件(下文将会详细描述)包含了用于浏览和选取文件所必需的基本代码。有了它们,你就不必为这些功能编写任何代码,进而能够专心实现打开或者保存文件等具体操作。

注意,FileDialog 类的 FilterIndex 属性(由于继承的原因,为 OpenFileDialog 和 SaveFileDialog 类所共有) 使用 one-based 索引(译者注:指从 1 开始编号的索引)。 此特性将在下文的代码中用到(并且会在相应位置再次强调)。当应用程序通过类型过滤器打开文件时,或者需要保存为特定格式的文件(比如:保存为纯文本文件而不是二进制文件)时,这一点是非常重要的。人们在使用 FilterIndex 属性时却经常忘了它,因此现在务必要把它记住。

下列代码通过 Button 控件的 Click 事件调用 OpenFileDialog 组件。当用户选中某个文件,并且单击 OK 的时候,所选的文件将被打开。在本例中,文件内容将被显示在消息框内,以证实文件流被读入。

本例假设存在名为 Button1 的 Button 控件和名为 OpenFileDialog1 的 OpenFileDialog 控件。

‘ Visual Basic
‘ NOTE: You must import the following namespace:
‘ Imports System.IO
‘ Without this import statement at the beginning
‘ of your code, the example will not function.
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
If OpenFileDialog1.ShowDialog() = DialogResult.OK Then
Dim sr As New StreamReader(OpenFileDialog1.FileName)
MessageBox.Show(sr.ReadToEnd)
sr.Close()
End If
End Sub

// C#
// NOTE: You must import the following namespace:
// using System.IO;
// Without this import statement at the beginning
// of your code, the example will not function.
private void button1_Click(object sender, System.EventArgs e)
{
if(openFileDialog1.ShowDialog() == DialogResult.OK)
{
StreamReader sr = new StreamReader(openFileDialog1.FileName);
MessageBox.Show(sr.ReadToEnd());
sr.Close();
}
}

打开文件还可以使用 OpenFileDialog 组件的 OpenFile 方法,它将返回文件的每一个字节。在下面的例子中,一个 OpenFileDialog 组件将被实例化,它使用了 cursor 过滤器,以限定用户只能选取光标文件(扩展名为 .cur)。一旦某个 .cur 文件被选中,窗体的光标就被设成该文件描绘的光标形状。

本例假设存在名为 Button1 的 Button 控件。

‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
‘ Display an OpenFileDialog so the user can select a Cursor.
Dim openFileDialog1 As New OpenFileDialog()
openFileDialog1.Filter = “Cursor Files|*.cur”
openFileDialog1.Title = “Select a Cursor File”

‘ Show the Dialog.
‘ If the user clicked OK in the dialog and
‘ a .CUR file was selected, open it.
If openFileDialog1.ShowDialog() = DialogResult.OK Then
If openFileDialog1.FileName <> “” Then
‘ Assign the cursor in the Stream to the Form’s Cursor property.
Me.Cursor = New Cursor(openFileDialog1.OpenFile())
End If
End If
End Sub

// C#
private void button1_Click(object sender, System.EventArgs e)
{
// Display an OpenFileDialog so the user can select a Cursor.
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.Filter = “Cursor Files|*.cur”;
openFileDialog1.Title = “Select a Cursor File”;

// Show the Dialog.
// If the user clicked OK in the dialog and
// a .CUR file was selected, open it.
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
if(openFileDialog1.FileName != “”)
{
// Assign the cursor in the Stream to the Form’s Cursor property.
this.Cursor = new Cursor(openFileDialog1.OpenFile());
}
}
}

关于读取文件流的进一步信息,请参阅FileStream.BeginRead 方法。

SaveFileDialog 组件

本对话框允许用户浏览文件系统并且选取将被写入的文件。当然,你还必须为文件写入编写具体代码。

下列代码通过 Button 控件的 Click 事件调用 SaveFileDialog 组件。当用户选中某个文件,并且单击 OK 的时候,RichTextBox 控件里的内容将被保存到所选的文件中。

本例假设存在名为 Button1 的 Button 控件,名为 RichTextBox1 的 RichTextBox 控件和名为 OpenFileDialog1 的 SaveFileDialog 控件。

‘ Visual Basic
‘ NOTE: You must import the following namespace:
‘ Imports System.IO
‘ Without this import statement at the beginning
‘ of your code, the code example will not function.
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
If SaveFileDialog1.ShowDialog() = DialogResult.OK Then
RichTextBox1.SaveFile(SaveFileDialog1.FileName, _
RichTextBoxStreamType.PlainText)
End If
End Sub

// C#
// NOTE: You must import the following namespace:
// using System.IO;
// Without this import statement at the beginning
// of your code, the code example will not function.
private void button1_Click(object sender, System.EventArgs e)
{
if((saveFileDialog1.ShowDialog() == DialogResult.OK)
{
richTextBox1.SaveFile(saveFileDialog1.FileName, RichTextBoxStreamType.PlainText);
}
}

保存文件还可以用 SaveFileDialog 组件的 OpenFile 方法,它将提供一个用于写入的 Stream 对象。

在下面的例子中,有一个包含图片的 Button 控件。 当你单击这个按钮的时候,一个 SaveFileDialog 组件将被打开,它将使用 .gif 、 .jpeg 和 .bmp 类型的文件过滤器。一旦用户通过 Save File 对话框内选中此类文件,按钮上的图片将被存入其中。

本例假设存在名为 Button2 的 Button 控件,并且它的 Image 属性被设为某个扩展名为 .gif 、 .jpeg 或者 .bmp 的图片文件。

‘Visual Basic
‘ NOTE: You must import the following namespaces:
‘ Imports System.IO
‘ Imports System.Drawing.Imaging
‘ Without these import statements at the beginning of your code,
‘ the code example will not function.
Private Sub Button2_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button2.Click
‘ Display an SaveFileDialog so the user can save the Image
‘ assigned to Button2.
Dim saveFileDialog1 As New SaveFileDialog()
saveFileDialog1.Filter = “JPeg Image|*.jpg|Bitmap Image|*.bmp|Gif Image|*.gif”
saveFileDialog1.Title = “Save an Image File”
saveFileDialog1.ShowDialog()

‘ If the file name is not an empty string open it for saving.
If saveFileDialog1.FileName <> “” Then
‘ Save the Image via a FileStream created by the OpenFile method.
Dim fs As FileStream = CType(saveFileDialog1.OpenFile(), FileStream)
‘ Save the Image in the appropriate ImageFormat based upon the
‘ file type selected in the dialog box.
‘ NOTE that the FilterIndex property is one-based.
Select Case saveFileDialog1.FilterIndex
Case 1
Me.button2.Image.Save(fs, ImageFormat.Jpeg)

Case 2
Me.button2.Image.Save(fs, ImageFormat.Bmp)

Case 3
Me.button2.Image.Save(fs, ImageFormat.Gif)
End Select

fs.Close()
End If
End Sub

// C#
// NOTE: You must import the following namespaces:
// using System.IO;
// using System.Drawing.Imaging;
// Without these import statements at the beginning of your code,
// the code example will not function.
private void button2_Click(object sender, System.EventArgs e)
{
// Display an SaveFileDialog so the user can save the Image
// assigned to Button2.
SaveFileDialog saveFileDialog1 = new SaveFileDialog();
saveFileDialog1.Filter = “JPeg Image|*.jpg|Bitmap Image|*.bmp|Gif Image|*.gif”;
saveFileDialog1.Title = “Save an Image File”;
saveFileDialog1.ShowDialog();

// If the file name is not an empty string open it for saving.
if(saveFileDialog1.FileName != “”)
{
// Save the Image via a FileStream created by the OpenFile method.
FileStream fs = (FileStream)saveFileDialog1.OpenFile();
// Save the Image in the appropriate ImageFormat based upon the
// File type selected in the dialog box.
// NOTE that the FilterIndex property is one-based.
switch(saveFileDialog1.FilterIndex)
{
case 1 :
this.button2.Image.Save(fs,ImageFormat.Jpeg);
break;

case 2 :
this.button2.Image.Save(fs,ImageFormat.Bmp);
break;

case 3 :
this.button2.Image.Save(fs,ImageFormat.Gif);
break;
}

fs.Close();
}
}

关于写入文件流的进一步信息,请参阅 FileStream.BeginWrite 方法。

ColorDialog 组件

此对话框显示颜色列表,并且返回所选的颜色。

与前两种对话框不同,ColorDialog 组件很容易实现其主要功能(挑选颜色)。选取的颜色将成为 Color 属性的设定值。因此,使用颜色就和设定属性值一样简单。在下面的例子中,按钮控制的 Click 事件将会开启一个 ColorDialog 组件。一旦用户选中某种颜色,并且单击了 OK ,按钮的背景将被设成所选的颜色。本例假设存在名为 Button1 的 Button 组件和名为 ColorDialog1 的 ColorDialog 组件。

‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
If ColorDialog1.ShowDialog() = DialogResult.OK Then
Button1.BackColor = ColorDialog1.Color
End If
End Sub

// C#
private void button1_Click(object sender, System.EventArgs e)
{
if(colorDialog1.ShowDialog() == DialogResult.OK)
{
button1.BackColor = colorDialog1.Color;
}
}

ColorDialog 组件具有 AllowFullOpen 属性。当其设为 False 的时候,Define Custom Colors 按钮将会失效,此时用户只能使用预定义的调色板。此外,它还有一个 SolidColorOnly 属性,当其设为 true 时,用户将不能使用抖动颜色。

FontDialog 组件

此对话框允许用户选择字体,以改变其 weight 和 size 等属性。

被选中的字体将成为 Font 属性的设定值。因此,使用字体也和设定属性值一样简单。在本例通过 Button 控件的 Click 事件调用 FileDialog 组件。当用户选中一个字体,并且单击 OK 的时候,TextBox 控件的 Font 属性将被设成所选的字体。本例假设存在名为 Button1 的 Button 控件,名为 TextBox1 的 TextBox 控件和名为 FontDialog1 的 FontDialog 组件。

‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
If FontDialog1.ShowDialog() = DialogResult.OK Then
TextBox1.Font = FontDialog1.Font
End If
End Sub

// C#
private void button1_Click(object sender, System.EventArgs e)
{
if(fontDialog1.ShowDialog() == DialogResult.OK)
{
textBox1.Font = fontDialog1.Font;
}
}

FontDialog 元件还包括 MinSize 和 MaxSize 属性,它们决定了允许用户选择的字体的最小和最大点数;还有一个 ShowColor 属性,当其设为 True 时,用户可以从对话框的下拉列表中选取字体的颜色。

PrintDocument 类

以下三个会话框,PrintDialog 组件、 PageSetupDialog 组件和 PrintPreviewDialog 控件,都与 PrintDocument 类有关。PrintDocument 类用于文档打印前的设置:设定其属性,以改变文档外观和打印方式,再将其实例输出到打印机。通常的步骤是:
(1) 生成 PrintDocument 类的一个实例;
(2) 设置 PageSetupDialog 组件的属性;
(3) 使用 PrintPreviewDialog 控件进行预览;
(4) 通过 PrintDialog 组件打印出来。

关于 PrintDocument 类的进一步资料,请参阅 PrintDocument Class 。

PrintDialog 元件

此对话框允许用户指定将要打印的文档。除此之外,它还能用于选择打印机、决定打印页,以及设置打印相关属性。通过它可以让用户文档打印更具灵活性:他们既能打印整个文档,又能打印某个片断,还能打印所选区域。

使用 PrintDialog 组件时要注意它是如何与 PrinterSettings 类进行交互的。PrinterSettings 类用于设定纸张来源、分辨率和加倍放大等打印机特征属性。每项设置都是 PrinterSettings 类的一个属性。通过 PrintDialog 类可以改变关联到文档的 PrinterSetting 类实例(由PrintDocument.PrinterSettings 指定)的特征属性值。

PrintDialog 组件将包含特征属性设置的 PrintDocument 类的实例提交到打印机。应用 PrintDialog 组件进行文档打印的范例,请参见 Creating Standard Windows Forms Print Jobs。

PageSetupDialog 组件

PageSetupDialog 组件用于显示打印布局、纸张大小和其它页面选项。如同其他对话框一样,可以通过 ShowDialog 方法调用 PageSetupDialog 组件。此外,必须生成一个 PrintDocument 类的实例,也即被打印的文档;而且必须安装了一台本地或者远程打印机,否则,PageSetupDialog 组件将无法获取打印格式以供用户选择。

使用 PageSetupDialog 组件时必须注意它是如何与 PageSettings 类进行交互的。PageSettings 类决定页面如何被打印,比如打印方向、页面大小和边距等。每项设置都是 PageSettings 类的一个属性。PageSetupDialog 类可以改变 PageSettings 类实例(由 PrintDocument.DefaultPageSettings 指定)的上述选项。

在下列代码中,Button 控件的 Click 事件处理程序开启一个 PageSetupDialog 组件;其 Document 属性被设成某个存在的文档;其 Color 属性被设成 false 。

本例假设存在名为 Button1 的 Button 控件、名为 myDocument 的 PrintDocument 控件和名为 PageSetupDialog1 的 PageSetupDialog 组件。

‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
‘ The print document ‘myDocument’ used below
‘ is merely for an example.
‘You will have to specify your own print document.
PageSetupDialog1.Document = myDocument
‘ Set the print document’s color setting to false,
‘ so that the page will not be printed in color.
PageSetupDialog1.Document.DefaultPageSettings.Color = False
PageSetupDialog1.ShowDialog()
End Sub

// C#
private void button1_Click(object sender, System.EventArgs e)
{
// The print document ‘myDocument’ used below
// is merely for an example.
// You will have to specify your own print document.
pageSetupDialog1.Document = myDocument;
// Set the print document’s color setting to false,
// so that the page will not be printed in color.
pageSetupDialog1.Document.DefaultPageSettings.Color = false;
pageSetupDialog1.ShowDialog();
}

PrintPreviewDialog 控件

与其他对话框不同,PrintPreviewDialog 控件对整个应用程序或者其它控件没有影响,因为它仅仅在对话框里显示内容。此对话框用于显示文档,主要是打印之前的预览。

调用 PrintPreviewDialog 控件,也是使用 ShowDialog 方法。同时,必须生成 PrintDocument 类的一个实例,也即被打印的文档。

注意:当使用 PrintPreviewDialog 控件时,也必须已经安装了一台本地或者远程打印机,否则 PrintPreviewDialog 组件将无法获取被打印文档的外观。

PrintPreviewDialog 控件通过 PrinterSettings 类和 PageSettings 类进行设置,分别与 PageDialog 组件和 PageSetupDialog 组件相似。此外,PrintPreviewDialog 控件的 Document 属性所指定的被打印文档,同时作用于 PrinterSettings 类和 PageSettings 类,其内容被显示在预览窗口中。

在下列代码中,通过 Button 控件的 Click 事件调用 PrintPreviewDialog 控件。被打印文档在 Document 属性中指定。注意:代码中没有指定被打印文档。

本例假设存在名为 Button1 的 Button 控件,名为 myDocument 的 PrintDocument 组件和名为 PrintPreviewDialog1 的 PrintPreviewDialog 控件。

‘ Visual Basic
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
‘ The print document ‘myDocument’ used below
‘ is merely for an example.
‘ You will have to specify your own print document.
PrintPreviewDialog1.Document = myDocument
PrintPreviewDialog1.ShowDialog()
End Sub

// C#
private void button1_Click(object sender, System.EventArgs e)
{
// The print document ‘myDocument’ used below
// is merely for an example.
// You will have to specify your own print document.
printPreviewDialog1.Document = myDocument;
printPreviewDialog1.ShowDialog()
}

小结
.net 框架里包含了 Windows 用户所熟悉的各种公共对话框,于是在应用程序中提供交互功能变得更加容易。通常,对话框的用途是多种多样的;.NET 框架对此提供了开放支持,你可以选择最佳方案以适合应用程序的需要。我们介绍了与对话框组件有关的一些简单应用。你可以直接使用这些代码,也可以对其稍加修改用于你的应用程序。

   这里我给大家提供一个很实用的例子,就是在线发送ICQ信息。想一想我们在网页上直接给朋友发送ICQ信息,那是多么美妙的事情啊。呵呵,在吹牛啊,其实ICQ本来就有在线发送的代码,不过,这些都是AOL给你写好的代码,多没有意思啊。还是自已写的比较好,呵呵,废话少说,大家来看代码吧
<% @ Page Langua
ge=”C#” %>
<% @ Assembly Name="System.Net" %>
<% @ Import Namespace="System.Net" %>
<% @ Import Namespace="System.Net.Sockets" %>
<% @ Import Namespace="System.IO" %>
<% @ Import Namespace="System.Text" %>












     
发送ICQ信息
您的ICQ号:
您的网名:
您的Email:
发送主题:
发送内容:

//online.cs(用户在线检测)
/*程序实现思路:


该用户有以下几个属性:
name:用户名
sessionID:用户ID,通过它唯一表示一个用户
iswhere :附加信息,用户当前所在位置
lasttime:用户登陆时间
curtime:本次刷新时间


在客户端,使用一个IFRAME,装载一个刷新页面,每隔XX秒更新一下他的名字对应的curtime,就表示他仍然在


在服务器端,建立一个守护线程,每隔固定时间就运行一遍,然后判断当前所有用户列表中的时间间隔是否超出了规定的时间,如果超出,则将该用户从在线列表中删除,这样就可以做到检测用户是否在线了,而如果再单独
写个用户离线后的处理,就可以解决好多人问到的:用户意外吊线后的处理。
*/


#define DEBUG


using System;
using System.Data;
using System.Data.SqlClient;
using System.Collections ;
using System.Threading ;
using System.Web;
using System.Diagnostics;


namespace SohoProject
{
    //定义了一个结构
    public struct User
    {
     public string name;
     public DateTime lasttime; 
     public DateTime curtime;
     public string sessionid;
  public string ip;
     public string iswhere;
    }


 public class OnLineUser
 {
  private static DataTable _alluser;
 
  //只读属性
  public DataTable alluser{
   get{return _alluser;}
  }


  public OnLineUser()
  {
   if(_alluser==null)
   {
    //define user list
    // Declare variables for DataColumn and DataRow objects.
    _alluser = new DataTable(“onlineuser”);


    DataColumn myDataColumn;
 
    // Create new DataColumn, set DataType, ColumnName and add to DataTable.   
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.String”);
    myDataColumn.ColumnName = “name”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “name”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = false;
    _alluser.Columns.Add(myDataColumn);
 
 
    // Create sessionid column.
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.String”);
    myDataColumn.ColumnName = “sessionid”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “sessionid”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = true;
    _alluser.Columns.Add(myDataColumn);


    // Create ip column.
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.String”);
    myDataColumn.ColumnName = “ip”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “ip”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = false;
    _alluser.Columns.Add(myDataColumn);


    // Create iswhere column.
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.String”);
    myDataColumn.ColumnName = “iswhere”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “iswhere”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = false;
    _alluser.Columns.Add(myDataColumn);


    // Create iswhere column.
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.DateTime”);
    myDataColumn.ColumnName = “lasttime”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “lasttime”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = false;
    _alluser.Columns.Add(myDataColumn);


    // Create iswhere column.
    myDataColumn = new DataColumn();
    myDataColumn.DataType = System.Type.GetType(“System.DateTime”);
    myDataColumn.ColumnName = “curtime”;
    myDataColumn.AutoIncrement = false;
    myDataColumn.Caption = “curtime”;
    myDataColumn.ReadOnly = false;
    myDataColumn.Unique = false;
    _alluser.Columns.Add(myDataColumn);
   }
  }



  //功能说明:将当前用户加入在线列表
  //如果该用户的数据当前仍然在在线列表中,则暂时先不让该用户登陆,提示用户存在
  public bool  AddUserToOnLine(User user)
  {
#if DEBUG
   (new SohoProject.SohoDebug()).WriteToDoc(“开始进入<将当前用户加入在线列表>….”);
   (new SohoProject.SohoDebug()).WriteToDoc(“\r\n”);
#endif



   //开始搜索是否已经存在该用户,如果存在则是改变数据,否则添加新的用户
   string strExpr;
   strExpr = “sessionid=’” + user.sessionid + “‘”;
   DataRow[] curUser;
   // Use the Select method to find all rows matching the filter.
#if DEBUG
   (new SohoProject.SohoDebug()).WriteToDoc(“搜索字符串:” + strExpr);
   (new SohoProject.SohoDebug()).WriteToDoc(“\r\n”);
#endif



   curUser = _alluser.Select(strExpr);


#if DEBUG
   (new SohoProject.SohoDebug()).WriteToDoc(strExpr);
   (new SohoProject.SohoDebug()).WriteToDoc(curUser.Length.ToString());
#endif



   if (curUser.Length >0 )
   {
    for(int i = 0; i < curUser.Length; i ++)
    {
     curUser[i]["curtime"]=DateTime.Now;
     curUser[i]["iswhere"]=user.iswhere;
    }
   }
   else
   {
    //直接加入新的数据
    DataRow myRow;
    try
    {
     myRow = _alluser.NewRow();
     // Then add the new row to the collection.
     myRow["name"] = user.name;
     myRow["ip"] = user.ip;
     myRow["iswhere"] = user.iswhere;
     myRow["lasttime"] = user.lasttime;
     myRow["curtime"] = DateTime.Now;
     myRow["sessionid"] = user.sessionid;
     _alluser.Rows.Add(myRow);
    }
    catch(Exception e)
    {
     throw(new Exception(e + “——————–” +  e.ToString())) ;
    }
   }
   _alluser.AcceptChanges();
   return true;
  } 
 



  //功能说明:判断某用户是否在线,本部分暂时不用
  //返回值:TRUE代表在线,FALSE不在
  public  Boolean IsUserOnLine(string name)
  {
   //需要先判断用户是否已经在用户列表中了
   //开始搜索是否已经存在该用户,如果存在则是改变数据,否则添加新的用户
   string strExpr;
   strExpr = “name =’” + name + “‘”;
   DataRow[] curUser;
   // Use the Select method to find all rows matching the filter.
   curUser = _alluser.Select(strExpr);


   if (curUser.Length >0 )
   {
    return true;   
   }
   else
   {
    return false;
   }
  }
       
  //功能说明:更新用户在线时间
  //返回值:最新的在线用户列表
  public Boolean CheckUserOnLine(string name,string iswhere,string sessionid,string ip)
  {
#if DEBUG
   (new SohoProject.SohoDebug()).WriteToDoc(“开始进入检查用户方法….”);
   (new SohoProject.SohoDebug()).WriteToDoc(“”);
#endif


   //需要先判断用户是否已经在用户列表中了
   User newuser=new User();
   newuser.name= name;
   newuser.iswhere= iswhere;
   newuser.lasttime=newuser.curtime=DateTime.Now;
   newuser.sessionid=sessionid;
   newuser.ip=ip;


   OnLineUser alluser= new OnLineUser();
   alluser.AddUserToOnLine(newuser);



#if DEBUG
   (new SohoProject.SohoDebug()).WriteToDoc(“离开检查用户方法….”);
#endif


   return true;
  }
 }
   
    //定义在线用户类
    public class OnLineUser_old
    {
     private static ArrayList _alluser ;  //定义用户
    
        public ArrayList alluser
        {
         get{return _alluser;}
         set{_alluser=value;}
        }
       
        public OnLineUser_old()  //构造函数
        {
         if(_alluser==null)
         {
          _alluser=new ArrayList();
         }
        }


        //功能说明:将当前用户加入在线列表
        //如果该用户的数据当前仍然在在线列表中,则暂时先不让该用户登陆,提示用户存在
        public bool  AddUserToOnLine(User user)
        {
         //需要先判断用户是否已经在用户列表中了
         if(_alluser==null)
         {
          _alluser.Add(user);
          return (true);
         }
         else
         {
    for ( int i = 0 ; i < _alluser.Count ; i ++)
          {
           //循环判断用户是否已经存在           SohoProject.User tempuser = (SohoProject.User)_alluser[i] ;


           if( tempuser.sessionid.Equals(user.sessionid))
           {
      //更新用户在线时间
      tempuser.name=user.name;
      tempuser.curtime=DateTime.Now;
      tempuser.iswhere=user.iswhere;
      tempuser.sessionid=user.sessionid;
      tempuser.ip=user.ip;
      alluser[i]=tempuser;
      return(true);
      //return(true); //用户已经存在,则直接退出           }
         }
          _alluser.Add(user);
          return (true);
         }
        } 
       
        //功能说明:判断某用户是否在线,本部分暂时不用
  //返回值:TRUE代表在线,FALSE不在
        public  Boolean IsUserOnLine(string name)
        {
         //需要先判断用户是否已经在用户列表中了
         if(_alluser==null)
         {
          return (false);
         }
         else
         {
          for ( int i = 0 ; i < _alluser.Count ; i ++)
          {
           //循环判断用户是否已经存在           SohoProject.User tempuser = (SohoProject.User)_alluser[i] ;
           if(tempuser.name.ToLower().Equals(name.ToLower()))
           {
            return(true) ;
           }
         }
          return (false);
         }
        }
       
        //功能说明:更新用户在线时间
  //返回值:最新的在线用户列表
        public Boolean CheckUserOnLine(string name,string iswhere,string sessionid,string ip)
        {
         //需要先判断用户是否已经在用户列表中了
         if(_alluser!=null)
         {
          User newuser=new User();
    newuser.name= name;
    newuser.iswhere= iswhere;
    newuser.lasttime=newuser.curtime=DateTime.Now;
    newuser.sessionid=sessionid;
    newuser.ip=ip;
   
    //OnLineUser alluser= new OnLineUser();
    AddUserToOnLine(newuser);
   }
   return(false);
        }
    }
   
   
   
    /*
    下面开始建立守护线程类:
    (注:此处,开始写的时候本来想做成单件模式的,不过由于以前没有做过这个东西,所以反而发生
    了很多问题,最后决定放弃而使用现有的格式)
    */
    public class CheckOnline
    {
     const int DELAY_TIMES = 10000 ;    //定义执行的时间间隔为5秒
     const int DELAY_SECONDS=60;     //将用户掉线时间设置为30秒
    
  private Thread thread ;      //定义内部线程
  private static bool _flag=false;   //定义唯一标志
 
  public CheckOnline()
  {
         if (!_flag)
         {
          _flag= true;
             this.thread = new Thread(new ThreadStart(ThreadProc)) ;
             thread.Name = “online user” ;
             thread.Start() ;
            }
        }
 
 
        internal void ThreadProc()
        {
            while(true) 
            {
//              SohoProject.OnLineUser temp=new SohoProject.OnLineUser();  //定义一个用户对象
//      for (int i=0 ;i< temp.alluser.Count;i++)
//          {
//           User tmpuser=(User)temp.alluser[i];
//           //我是将该用户的最新时间加上30秒,然后和当前时间比较,小与当前时间,
//           //则表示该用户已经吊线,则删除他的记录
//           if(tmpuser.curtime.AddSeconds(DELAY_SECONDS).CompareTo(DateTime.Now)<0)
//           {
//            temp.alluser.RemoveAt(i);
//           }
//          }


   


    SohoProject.OnLineUser temp=new SohoProject.OnLineUser();  //定义一个用户对象
    //开始检查是否有用户过期了    string strExpr;
    //tmpuser.curtime.AddSeconds(DELAY_SECONDS).CompareTo(DateTime.Now)<0
    strExpr = “curtime < ‘” + DateTime.Now.AddSeconds( 0 – DELAY_SECONDS) + “‘”;
#if DEBUG
    (new SohoProject.SohoDebug()).WriteToDoc(strExpr);
#endif


    DataRow[] curUser;
    // Use the Select method to find all rows matching the filter.
    curUser = temp.alluser.Select(strExpr);


    if (curUser.Length >0 )
    {
     //删除这些记录
     for(int i = 0; i < curUser.Length; i ++)
     {
      curUser[i].Delete();
     }
     temp.alluser.AcceptChanges();
    }
                Thread.Sleep(DELAY_TIMES) ;
            }
        }
    }
}

产品简介

您看到过出色的咖啡店店员送咖啡的情景吗?那简直就是咖啡豆、蒸汽和牛奶调和咖啡饮料在跳精彩的芭蕾,跳跃着奔向焦急等候的顾客。然而,即便是最好的店员偶尔也会出现问题。比如两个单子在处理时搞混了,结果送到您面前的是一杯 Soy latte。也可能是杯子上龙飞凤舞的潦草字迹根本就是写错了,或者店员理解错了。有人要了一杯“卞高奇若”(卡普其诺),可怜的店员绞尽脑汁也弄不懂顾客到底要点什么。如果出现了类似的问题,就必须停止处理,然后再重新开始。好的服务员可能会推延一下现有的要求,而优秀的服务员却能够在没人察觉的情况下做到这一点。

Microsoft? ASP.NET 在系统可靠性方面取得了优于其任何竞争对手的巨大进步。然而,就像那位出色的店员一样,ASP.NET 偶尔也会出现问题。幸运的是,ASP.NET 是非常优秀的服务器。它能在后台迅速生成新的进程,然后处理请求。通常只会在请求页面时发生一点用户甚至可能都不会注意到的轻微延迟。

而 ASP.NET 系统的管理员可能需要知道发生了什么。同样,他们也想了解是什么原因导致了进程失败。幸运的是,使用 .net Framework 类库文档中的 ProcessInfo 和 ProcessModelInfo 类便可获得相关信息。本文中,我们将学习如何创建 ASP.NET HTTP 处理程序,以使用这些对象查看 Web 站点使用的进程的运行状况和关闭状况。另外,我们将创建一个配置节处理程序,这样我们便能够在安装处理程序后对其进行配置。

我们将看到什么?

ASP.NET 进程负责编译和管理所有向 ASP.NET 页面提出的请求。理想状况下,此进程应该始终存在于服务器中:活跃地接收请求、编译页面并返回 HTML。然而,由于存在许多可能影响进程的潜在事件,我们不得不面对 Web 开发的真实状况。开发人员可能未能正确处理内存泄漏或线程问题;服务器可能会丢失与进程的连接;或者甚至会因为在 Web.config 文件的 Element 节中对 idleTimeout、requestLimit、memoryLimit 和类似的项目进行了错误的配置而导致出现问题。如果发生了以上任何一种事件,则将创建新的 ASP.NET 辅助进程,新的请求将移交至此进程进行处理。

由于 ASP.NET 进程对页面处理如此重要,因此监视这些进程同样重要。使用 ProcessInfo 和 ProcessModelInfo 类可以查看当前和以前进程的有效期和运行状况。图 1 所示为在本文中创建的进程列表。

ProcessInfo class 存储了给定进程的数据。不得自行创建 ProcessInfo 类,但可以使用 ProcessModelInfo class 来检索 ProcessInfo 对象。表 1 所示为 ProcessInfo 类的重要属性。

表 1:ProcessInfo 类的属性

属性 数据类型 说明
Age TimeSpan 进程运行(或曾经运行)的总时间。如果这个值超出了在 Web.Config 文件的 processModel 节中的超时设置,可导致重新启动进程。
PeakMemoryUsed Integer 此进程所用内存的最大值(以 MB 为单位)。如果这个值超出了在 Web.Config 文件的 processModel 节设置的 memoryLimit 级别设置,可导致进程重新启动。
ProcessID Integer 操作系统使用此 ID 来标识进程。每个进程均有唯一的 ID(在进程运行时)。
RequestCount Integer 进程接收到的页面请求的数量。如果这个值超出了在 Web.Config 文件的 processModel 中 requestLimit 的级别设置,可导致进程重新启动。
ShutdownReason ProcessShutdownReason 此枚举定义进程重新启动的可能原因。有关可能的值,请参阅表 2。
StartTime DateTime 进程启动的时间。
Status ProcessStatus 此枚举定义 ASP.NET 辅助进程的当前状态。此值可能为 Alive(活动)、ShuttingDown(进程已接收到关闭请求)、ShutDown(进程已正常关闭)或 Terminated(进程已被迫关闭)。

某进程关闭后,关闭原因将被设置为 ProcessShutdownReason Enumeration 中的某一个值。

表 2:进程关闭的可能原因

说明
None 此值表明进程仍在运行。
Timeout 进程因其生存期超出了在 Web.Config 文件的 processModel 节中设置的超时值而重启。如果这种情况频繁发生,也许应考虑增加超时值。不过,一般来说因这种原因而重新启动可以接受。
IdleTimeout 进程重新启动的原因是缺少客户端。如果在 Web.Config 文件的 processModel 节中的 idleTimeout 值所设置的时间期限内没有客户端请求,将发生此类重新启动。这通常也是可以接受的重新启动的原因。
RequestsLimit 进程重新启动的原因是接收到的请求数量超过了在 Web.Config 文件的 processModel 节中设置的值 (requestLimit)。一般来说,这种重新启动的原因也可以接受,主要用于您希望进程偶尔重新启动的情况。
MemoryLimitExceeded 进程重新启动的原因是因为超出了通过 Web.Config 文件中的 memoryLimit 值设置的内存限制。这通常表示进程的某个 ASP.NET 应用程序部分发生了问题(可能是内存泄漏)。如果此类现象频繁发生,请监视每个 Web 应用程序的内存使用是否正常。
RequestQueueLimit 进程重新启动的原因是在等候响应的请求总数超出了 Web.Config 文件的 requestQueueLimit 值。通常这是某些情况将导致 Web 服务器延迟的信号。可能需要增加内存或服务器,或提高驱动器或处理器的速度。
DeadlockSuspected 进程重新启动的原因是可能停止了正在处理的请求。正如这个关闭原因的名称那样,最可能导致这种情况的原因为:如果两个或多个线程需要另一个线程完成后才能继续进行(例如 A 线程需要 B 线程完成向某文件的写入后才能继续进行,而同时 B 线程需要 A 线程完成计算后才能继续进行),我们将这种情况称为线程处于“Deadlock”(死锁)状态。如果有这种可能,进程将因此而关闭。一般来说,您肯定不希望看到这种关闭原因,如果您不幸看到了,请查看在应用程序中使用的所有线程处理或资源使用情况。
PingFailed 当 ASP.NET 辅助进程管理页面时,有时会收到从 IIS 进程发来的 ping 以确定是否仍需要此进程。如果 ping 失败,则 IIS 进程可能会关闭该 ASP.NET 进程。这个关闭原因说明了可能在服务器接收消息的过程中确实存在通信问题或 ASP.NET 辅助进程因某种原因而停止工作。
Unexpected 一般来说,您肯定不想看到此消息,因为它表明是“某种其他原因”终止了 ASP.NET 辅助进程。除了监视每个进程或对所有运行中的代码执行代码校对,几乎没有任何办法解决此问题。

创建进程查看处理程序

在 ASP.NET 中,主要使用两种方法来创建 HTTP 处理程序。第一种是通过创建带有 ASHX 扩展名的文件,另一种是创建实现 System.Web.IHttpHandler 的类,请参阅 IHttpHandler Interface。本文将着重介绍第二种形式。要创建 HTTP 处理程序,需要创建一个程序集(通常是一个代码库项目)和一个实现 System.Web.IHttpHandler 的类。然后将该类注册到 Web.Config(或 machine.config)文件中,然后它就可以接收请求了。如果查看 machine.config 文件(在相应命名的 httpHandlers 节中),将看到许多当前已注册的 HTTP 处理程序,包括 System.Web.UI.PageHandlerFactory(ASP.NET 页面的主处理程序)。在编写 HTTP 处理程序时,其实就是在定义处理请求的新方法。

所有 HTTP 处理程序均通过实现 System.Web.IHttpHandler Interface 来创建。此接口需要创建一个属性和一个方法,如表 3 所示。

表 3:IHttpHandler 接口的成员

成员 类型 说明
IsReusable 属性 (Boolean) 确定该处理程序的实例是否可以重复使用。通常,该属性应返回 true,除非处理程序需要对某个资源的独占访问。
ProcessRequest 方法 HTTP 处理程序的“主”方法。将在此添加对请求的所有处理。该类传递当前 ASP.NET 上下文。可以从此上下文中检索请求对象和响应对象。

实现 IHttpHandler

创建 HTTP 处理程序的大量工作集中在实现处理程序的 ProcessRequest。通常,需要存储当前上下文的请求和响应对象,然后使用响应对象的编写方法创建输出。以下给出了用于进程查看处理程序的 ProcessRequest 资源的 Microsoft Visual Basic? .net 源。

 Public Sub ProcessRequest(ByVal context As HttpContext) _ Implements IHttpHandler.ProcessRequest _context = context _writer = New HtmlTextWriter(context.Response.Output) 'we only want to do this if we're enabled If _config.Enabled Then _writer.WriteLine("") _writer.WriteLine("") _writer.WriteLine(Me.StyleSheet) _writer.WriteLine("") _writer.WriteLine("") _writer.WriteLine("") 'write content here 'create table Dim t As New Table() With t .Width = Unit.Percentage(100) .CellPadding = 0 .CellSpacing = 0 End With 'the meat of the routine 'make certain this is a destination machine If (PermittedHost(_context.Request.UserHostAddress)) Then CreateHeader(t) AddProcesses(t) CreateFooter(t) Else CreateErrorReport(t) End If 'write to the stream t.RenderControl(_writer) _writer.WriteLine("\r\n\r\n") End If End Sub 

ProcessRequest 的实现会存储当前上下文和编写者。然后,它通过呈现页面的起始 HTML 标签将新的 HTML 页面创建为输出。下一步,它将创建一个用于格式化输出的表格。最后,如果启用了处理程序,并且发出请求的客户端是合法的 IP 地址之一,则通过以下三种方法创建输出: CreateHeaderAddProcessesCreateFooter。这些方法将相应的值呈现在表格的单元格中。这些代码有很大一部分是重复的,为了简短起见,以下仅给出了 AddProcesses 及其相关的方法。

 Private Sub AddProcesses(ByVal table As _ System.Web.UI.WebControls.Table) Dim procs As ProcessInfo() = _ ProcessModelInfo.GetHistory(_config.RequestLimit) Dim row As TableRow _list = New ProcessInfoCollection For Each proc As ProcessInfo In procs row = AddRow(table) _list.Add(proc) AddCell(row, proc.ProcessID.ToString()) AddCell(row, proc.Status.ToString()) AddCell(row, proc.StartTime.ToString("g")) AddCell(row, FormatAge(proc.Age)) AddCell(row, proc.PeakMemoryUsed.ToString("N0") + " MB") AddCell(row, proc.RequestCount.ToString("N0")) AddCell(row, proc.ShutdownReason.ToString()) Next End Sub Private Function AddCell( _ ByVal row As System.Web.UI.WebControls.TableRow, _ ByVal text As String) As System.Web.UI.WebControls.TableCell Dim c As New TableCell() c.Text = text row.Cells.Add? Return c End Function 

细心的(和有技术背景的)读者可能已经注意到,我完全可以通过呈现 DataGrid 并将 ProcessInfoCollection 绑定到 DataGrid 来简化此代码,但那样就失去了编写程序的乐趣。

安装 HTTP 处理程序

创建完 HTTP 处理程序后,必须进行安装才能使用。这包括使类可用,并在配置文件中添加相应的信息以激活处理程序。

如果创建的是仅被单个 vroot 使用的简单处理程序,则可以将 DLL 复制至该 vroot 的 bin 目录即可使用该类。如果创建了一个由多个 vroot 使用的 HTTP 处理程序(类似于 ProcessHandler),则此处理程序必须安装到全局程序集缓存 (GAC) 中。要将此处理程序安装到 GAC 中,类必须具有严格名称。要具有严格名称,它必须有关联的严格名称键。必须使用命令行可执行文件 sn.exe 创建严格名称键文件。有关此程序的详细信息,请参阅 NET Framework Tools 文档的 Strong Name Tool (Sn.exe) 一节。

处理程序可用后,下一步就是添加配置以使其可以处理请求,方法是在 Web.Config 或 machine.config 文件的 httpHandlers 节中添加条目。此条目指定了将通过处理程序路由的文件扩展名和操作。进程查看处理程序的条目如下所示。

 

此条目意味着在某个请求使用任何 HTTP 命令寻找“文件” process.axd(实际上不存在)时,它将向位于程序集 MsdnProcessHandler 中的 Microsoft.Samples.Msdn.Web.ProcessHandler 类发送请求。该类将实现 IHttpHandler,然后由 IHttpHandler 负责生成输出。

添加配置

许多 ASP.NET 应用程序使用 appSetting 标签添加自定义配置。这对于大多数应用程序来说已经完全足够了。然而,有时应用程序可以使用更有针对性的解决方案。这种情况下,您可以为应用程序新建节。

新建配置节包括两个步骤。首先,必须创建配置对象。此对象或结构具有表示所需配置数据的属性。此对象可以具有、但通常不具有任何方法。其次要创建一个节处理程序。此节处理程序负责从 web.congfig 文件中读取相应的信息,并且将其转化为配置对象。

ProcessViewer 的配置对象具有四个属性,如下表所述。

表 4:ProcessViewer 配置对象的属性

属性 数据类型 说明
Enabled Boolean 如果 ProcessViewer 可用,则为 true。这样便可以暂时关闭处理程序而无需将其从 web.config 文件中删除。
LocalOnly Boolean 如果只能从本地计算机查看 ProcessViewer 的输出,则为 true。这是最为安全的方案,防止其他人查看 Web 应用程序的进程历史记录。
RequestLimit Integer 该属性限定了显示项目数的最大值。ProcessModelInfo.GetHistory 最多返回 100 个项目。此属性用于在需要时减少此数量。
PermittedHosts String array 如果 LocalOnly 为 false,则任何计算机均可以访问 Process.axd handler 来查看应用程序的进程历史记录。这就可能会有安全风险。因此,可以分配允许访问处理程序的 IP 地址的列表。此属性可用于限制对管理员工作站的访问。

创建自定义配置的第二步是创建解释配置文件的 XML 的类,并使用该信息填充配置对象。此类必须实现 System.Configuration.IConfigurationSectionHandler 接口。此接口只有一个方法,称为 Create。以下给出了 ProcessViewerSectionHandler 的 Visual Basic .net 源(请参阅 C# 源的下载)。

 Public Function Create(ByVal parent As Object, _ ByVal configContext As Object, _ ByVal section As System.Xml.XmlNode) As Object _ Implements Configuration.IConfigurationSectionHandler.Create ' 节具有以下格式: '
 Dim result New ProcessViewerConfiguration() Dim config As New ConfigurationHelper(section) Dim max As Integer Dim hosts As String Const delimiter As String = ", " Const MaximumReturnCount As Integer = 100 '确认设置,并设定 result.Enabled = config.GetBooleanAttribute("enabled") result.LocalOnly = config.GetBooleanAttribute("localOnly") max = config.GetIntegerAttribute("requestLimit") If max <= MaximumReturnCount Then result.requestLimit = max End If hosts = config.GetStringAttribute("permittedHosts") result.PermittedHosts = hosts.Split(delimiter.ToCharArray()) Return result End Function 

Create 方法传递了三个对象:

  • parent – 表示父配置节(如果可用)。
  • configContext – 表示 HttpConfigurationContext 对象,即 Web 配置的剩余部分。可以使用此对象从当前 web.config 文件中检索值。
  • section – 最为重要的参数,实际的配置节。使用此对象填充配置对象。

以上代码使用了 ConfigurationHelper 对象。此对象是一个用于从节中检索特定数据类型的简单对象。此类的代码如下所示。

Friend Class ConfigurationHelper Dim _section As XmlNode Public Sub New(ByVal configSection As XmlNode) _section = configSection End Sub '接受 true/false、yes/no Public Function GetBooleanAttribute(ByVal name As String) As Boolean Dim value As String Dim result As Boolean value = GetStringAttribute(name).ToLower() If ((Boolean.TrueString.ToLower() = value) _ OrElse (value = "yes")) Then result = True Else result = False End If Return result End Function Public Function GetIntegerAttribute(ByVal name As String) As Integer Dim value As String Dim result As Integer value = GetStringAttribute(name) result = Int32.Parse(value) Return result End Function Public Function GetStringAttribute(ByVal name As String) As String Dim theAttribute As XmlAttribute Dim result As String theAttribute = _section.Attributes(name) If Not theAttribute Is Nothing Then result = theAttribute.Value End If Return result End Function End Class 

要使用这个节处理程序和配置对象,必须将其注册在相应的 ASP.NET 配置文件中。因为可以在任何进程中调用该类,所以最好将其添加到 machine.config 文件中。在 machine.config 类的 configSection 节中的相应位置注册节处理程序(我将其添加到了 system.web 节中)

  ... other sections 

注册完毕后,便可以向 machine.config 文件中添加节,并且该类将成为可配置类。

小结

创建 HTTP 处理程序可以提供超出 ASP.NET. 功能的强有力机制,使得开发者避开页面模型,并创建、修改或扩展 Web 站点的内容。通过添加用于查看 ASP.NET 的进程历史记录的 HTTP 处理程序,可以诊断代码或服务器中导致客户投诉的问题,例如代码中存在内存泄漏或未处理的异常,或服务器的内存不足。

创建并安装 HTTP 处理程序后,您将可以更敏锐地发现在此重要进程中发生的状况。到时候您就会有时间来喝杯咖啡,而不是忙于追查 Web 站点进程重新启动的原因了。