T.T Cinq

FF 02 30 F7 01 01 02 FE FD FF

ASP.NET可交互式位图窗体设计(2)

使我们的对象可序列化
    为在 ASP.NET 中使用可绘制对象类,我们需要对其再进行一项更改。这些类需要是可序列化的,以便能够在主要的 Web 页和生成该图像的 Web 页之间传递数据(后面将详述)。序列化是这样的过程:将某个类的数据以某种方式写入存储介质,以便存储和/或传递数据并在以后反序列化。反序列化是从序列化数据中重新创建对象的过程。我们会在将来的专栏中深入讨论这个问题。
  
    Dr. GUI 最开始作为 Windows 窗体应用程序编写此应用程序时,只使用了 .NET Framework 和操作系统预先分配的 Brushes 和 Pens 类中的可用常用画笔和笔。因为这些已经分配完毕,保持对它们的引用不会有任何妨碍,同时也无需对其进行 Dispose。
  
    但由于笔和画笔是非常复杂的对象,不能是可序列化的,因此 Dr. GUI 必须改变其策略,转而决定存储笔和画笔的颜色,然后在需要绘制和填充对象时动态创建笔和画笔。
  
    如何使之可序列化?
    序列化是 .NET Framework 的一个重要部分,因此也使序列化对象的工作变得很简单。
  
    我们只需使用 Serializable 属性标记一个类便可使之可序列化。(这与我们以前用于在枚举上将其标记为一套标志的属性是同一种属性。)在 C# 和 Visual Basic .NET 中的语法如下所示:
  
  
  C#
  
  [Serializable]
  class Foo // ...
  Visual Basic
  
  .NET _
  Class Foo ' ...
  
  
    注意:除了将类标记为可序列化外,还必须使类中包含的所有数据可序列化,否则在试图序列化数据时,序列化框架会引发一个异常。
  
    使容器可序列化
    .NET Framework 的一大优点是可以使容器类可序列化。这意味着如果将对象存储在可序列化的容器中,容器可以自动序列化对象。
  
    因此在本例中,DShapeList 类包含了两个 ArrayList 对象。由于 ArrayList 是可序列化的,因此要使 DShapeList 可序列化,只需将其标记为 Serializable 属性即可,如下所示:
  
  
  Visual Basic
  
  .NET _
  Public Class DShapeList
  Dim wholeList As New ArrayList()
  Dim filledList As New ArrayList()
  ' ...
  
  
  C#
  
  [Serializable]
  public class DShapeList {
  ArrayList wholeList = new ArrayList();
  ArrayList filledList = new ArrayList();
  
  
    假设我们放在 DShapeList 中的对象都是可序列化的,这时便可以使用单个语句序列化和反序列化整个列表!
  
    顺便说一下,这对于该应用程序的 Windows 窗体版本也是一个很好的改变,因为它使我们能够将绘图写入磁盘文件并重新加载。
  
    可绘制对象的三个版本;任何一个都可以在任何上下文中使用
    您可能已经注意到,我们有三种版本的可绘制对象代码:在 C# 和 Visual Basic .NET 中各有一个不使用我们在上面编写的 helper 方法的版本,另一个是 Visual Basic .NET 中使用 helper 方法的版本。
  
    在这里还有一点微小的差别:使用 helper 的文件中的数据类被标记为可序列化;其他文件中的数据类则没有标记为可序列化。
  
    但是,请注意下面很重要的一点:如果我们返回去并将所有文件中的数据类标记为可序列化,那么将能够在任何应用程序中使用任何类。我们将能够混合使用 C# 和 Visual Basic .NET。并且能够在 ASP.NET 应用程序中使用最初为 Windows 窗体应用程序编写的代码。
  
    这种简便的代码重用意味着您编写的代码更具价值,因为代码可以在很多不同的环境中重复使用。
在 Windows 窗体应用程序中使用可绘制对象
    我们已经讨论了可绘制对象类,下面谈谈如何在 Windows 窗体应用程序中使用这些类。首先谈一下 Windows 窗体应用程序是怎样工作的。
  
    Windows 窗体应用程序的主要部分
    简单的 Windows 窗体应用程序包含一个主窗口(或窗体),其中包含控件子项。如果您是一位 Visual Basic 程序员,就会发现这个模型非常熟悉。
  
    主窗口
    任何 Windows 窗体应用程序中的关键对象都是主窗口。该窗体将在应用程序的 static/Shared Main 方法中创建,如下所示。
  
    在一个简单的 Windows 窗体应用程序(例如我们所编写的)中,所有其他控件都是此主窗体的子项。
  
    按钮和文本框
    我们的窗体具有一套按钮和一些文本框。每个按钮有一个处理程序,可以向列表中添加形状,并绘制列表。所包含的文本框用于显示如何从窗体中获得输入。还有一个分组框,提供了有关文本框和相关按钮的可视指示。
  
    PictureBox
    左边是最重要的控件:PictureBox。这是绘制和显示图像的位置。在 Windows 应用程序中,您可能需要随时重绘图像 -- 例如,如果窗口被最小化或被其他窗口覆盖,则再次显示窗口时便需要进行重绘。
  
    在响应画图 (Paint) 消息时便会完成这种按需绘图,由父窗体窗口类中的一个事件处理程序处理。
  
    Windows 窗体应用程序中的主要例程
    我们简单看一下 Windows 窗体应用程序中的重要例程。请注意,用户界面的代码与可绘制对象的代码相比非常简短。这就是使用 .NET Framework 完成诸多工作的好处。(这也表明我们使用可绘制对象类完成的工作确实很好。)
  
    窗体方法
    窗体(或主窗口)是从 System.Windows.Forms.Form 中派生的,所以继承了其所有行为。所有这些控件都声明为这个类的成员,这样在清理类时它们也将被清理(清理是在 Dispose 方法中实际明确完成的)。
  
    它还包含了我们所需数据的声明(DShapeList 和一个随机数生成器对象)、Main 以及用于按钮单击事件和 PictureBox 画图事件的事件处理程序。
  
    Main
    Main 的任务就是创建和运行主窗口对象。其 C# 代码如下所示。
  
  
  C#
  
  [STAThread]
  static void Main()
  {
  Application.Run(new MainWindow());
  }
  
  
    STAThread 属性对于 Windows 窗体应用程序的 Main 非常重要 -- 您应当始终使用该项,以便依赖于 OLE Automation(例如拖放和剪贴板)的功能能够正常工作。
  
    在 Microsoft Visual Studio? 生成的 Visual Basic .NET 源代码中不会找到此方法,但是如果使用 ILDASM 在 .exe 中查找,便会找到一个与上面所述功能相同的 Main -- 可能是由 Visual Basic .NET 编译器生成的。
  
    InitializeComponent
    在 Windows Form Designer generated code(Windows 窗体设计器生成的代码)下(如果不能看到此区域中的代码,单击小加号),会看到用于创建和初始化所有按钮和窗体上其他控件的代码。
  
    数据声明/随机数生成
    除了在代码的隐藏区域中声明的所有控件外,我们还需要声明两个变量:存放绘图列表的数据结构,以及一个 Random 类型的对象。我们使用 Random 对象为所创建的对象的位置生成随机数。
  
    数据声明位于 MainWindow 类内,但位于任何方法之外。在 C# 和 Visual Basic .NET 中,其代码如下所示:
  
  
    C#
  DShapeList drawingList = new DShapeList();
  Random randomGen = new Random();
  
  
    Visual Basic
  
  .NET Dim drawingList As New DShapeList()
  Dim randomGen As New Random()
  
  
    我们还编写了一个 helper 方法以获得一个随机点:
  
  
     C#
  private Point GetRandomPoint() {
  return new Point(randomGen.Next(30, 320), randomGen.Next(30, 320));
  }
  
    Visual Basic
  
  .NET Private Function GetRandomPoint() As Point
  Return New Point(randomGen.Next(30, 320), randomGen.Next(30, 320))
  End Function
  
  
    它生成两个位于 30 和 320 之间的随机数,作为随机点的坐标。
按钮单击事件处理程序
    接下来就是每个按钮的按钮单击事件处理程序。多数仅仅是向绘图列表中添加一个新的可绘制对象,然后调用 PictureBox 上的 Invalidate,从而使用更新的绘图列表进行重绘。典型的按钮事件处理程序代码如下所示:
  
  
    C#
  private void AddPoint_Click(object sender, System.EventArgs e) {
  drawingList.Add(new DPoint(GetRandomPoint(), Color.Blue));
  Drawing.Invalidate();
  }
  
    Visual Basic
  
  .NET Private Sub AddPoint_Click(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles AddPoint.Click
  drawingList.Add(New DPoint(GetRandomPoint(), Color.Blue))
  Drawing.Invalidate()
  End Sub
  
  
    Change fills to hot pink(将填充色更改为粉红)按钮有一些不同 -- 它在列表中获得一个所有可填充对象的数组,然后将它们的画笔颜色更改为粉红。这部分代码显示在前面“返回可填充列表”一节的末尾。(此外还必须使 PictureBox 无效。)
  
    最后,Erase All(全部删除)按钮简单地创建了一个新的绘图列表,并将我们的 drawingList 字段指向该列表。这样便释放了旧的绘图列表以进行最后的内存回收。然后使 PictureBox 无效,把自己也删除掉。
  
    PictureBox 画图事件处理程序
    我们要注意的最后一项就是在 PictureBox 中画出图像。为此,需要处理 PictureBox 生成的 Paint 事件,然后使用通过此事件传递的 Graphics 对象在其上进行绘图。要进行绘图,只需调用绘图列表的 DrawList 方法 -- 一个 for each 循环和多态将负责处理剩下的工作!
  
  
    C#
  private void Drawing_Paint(object sender,
  System.Windows.Forms.PaintEventArgs e) {
  drawingList.DrawList(e.Graphics);
  }
  
  
    Visual Basic
  
  .NET Private Sub Drawing_Paint(ByVal sender As Object, _
  ByVal e As System.Windows.Forms.PaintEventArgs) _
  Handles Drawing.Paint
  drawingList.DrawList(e.Graphics)
  End Sub
  
  
    我们的 Windows 窗体应用程序之旅到此结束 -- 请斟酌这些代码并进行修改,这样可以学到更多内容!
  
    在 ASP.NET 应用程序中使用可绘制对象
    虽然 ASP.NET Web 应用程序和 Windows 窗体应用程序之间存在某些不同,但两者的相似性还是令 Dr. GUI 感到惊奇!
  
    Web 窗体应用程序的主要部分
    ASP.NET Web 窗体应用程序的主要部分与 Windows 窗体应用程序的各部分非常对应。
  
    页面
    此项对应 Windows 窗体应用程序中的主窗口。页面是所有按钮和其他控件的容器。
  
    按钮
    同样,这里有一组按钮,可用于在窗体上执行各种操作。请注意,与以前的应用程序不同,我们将页面文档的 pageLayout 属性设置为 GridLayout 而不是 FlowLayout。这意味着我们可以通过像素位置定位每个按钮(以及其他控件)。
  
    请注意,这里也有一些文本框。
  
    还要注意,您不能向 Web 复制和粘贴 Windows 窗体控件 -- 必须重新创建整个页面。
  
    图像控件
    图像控件对应于 Windows 窗体应用程序中的 PictureBox。但两者有一些重要的差别:图像控件不生成 Paint 消息,而是包含加载图像的 URL。
  
    我们将这个 URL 设置为第二个 Web 页,ImageGen.aspx。换句话说,我们有一个 Web 页,它的全部工作就是从我们的绘图列表中生成图像中的位,然后将图像发送到客户端的 Web 浏览器。
  
    我们将在下面讨论其代码。
  
    Web 窗体应用程序的主要例程
    Windows 窗体应用程序和 Web 窗体应用程序的代码之间存在一些重要不同 -- 但也有某些有趣的相似之处。还要注意,可绘制对象文件中的所有代码都可以用于三种应用程序中的任何一种。
  
    我们的页面是从 System.Web.UI.Page 派生的,除了以下内容外,还包含一组用于所有控件的声明:
  
    完全相同的内容:数据声明和 GetRandomPoint
    此代码与 Visual Basic .NET Windows 窗体应用程序中的代码几乎完全相同。如果愿意,可以再看一下上面的这段代码。它们之间只有一个不同之处,就是对字段进行了声明而没有将其初始化。它们将在 Page_Load 方法中被初始化(如后面所示)。
  
    GetRandomPoint 方法与其他应用程序完全相同。能够重复使用代码真的不错,不是吗?
  
  非常相似的内容:按钮单击事件处理程序
    按钮单击事件处理程序与 Windows 窗体应用程序相同,只有一个例外:在 Web 窗体中,由于每次单击按钮时都将重绘图像,因此无需(也不能)使图像控件无效。此外,它还将自动进行重绘 -- 因此唯一要调用的就是绘图列表的 Add 方法。
  
    以下是一个典型的按钮事件处理程序 -- 用于绘制一个点。
  
  Private Sub AddPoint_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles AddPoint.Click drawingList.Add(New DPoint(GetRandomPoint(), Color.Blue)) End Sub
  
    其他按钮事件处理程序都与 Windows 窗体的情况类似,当然,有一种例外情况除外,即不调用任何一种方法使图像无效。
  
    差别很大的内容:页面加载和卸载处理程序
    页面加载和卸载处理程序方法是完全不同的。
  
    请记住,我们的页面对象是使用每一个 HTTP 请求重新创建的。由于每个请求都将创建一个新页面,因此我们不能象在 Windows 窗体中那样将数据作为成员变量存储,在 Windows 窗体中,主窗口将伴随应用程序而存在。
  
    因此我们必须在某种状态变量中存储请求和页面之间所需的信息。这里有几种选择 -- 下面将就此进行讨论。 
  
在页面和请求之间传递状态
    为使应用程序能够工作,它需要能够维护请求之间的状态并将状态传递给绘图页面(如下所示)。
  
    维护和传递状态有多种方式。如果应用程序是严格的单页面应用程序(和以前的应用程序一样),则可以使用视图状态,其中数据被编码存储在 Web 页的隐藏输入字段中。
  
    但是我们的图像控件是在单独的页面中进行绘图的,因此需要某些更灵活的东西。最好的选择就是 cookie 和会话状态。会话状态非常灵活,但要求使用服务器资源。浏览器可以保留 cookie,但其大小非常有限。
  
    Page_Load
    Page_Load 是在创建页面对象之后以及在运行所有事件处理程序之前被调用的。因此 Page_Load 方法是加载永久数据的理想所在。如果找不到数据,就创建新的数据。以下是相关代码:
  
  
  Private Sub Page_Load(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) _
  Handles MyBase.Load
  randomGen = ViewState("randomGen")
  If randomGen Is Nothing Then randomGen = New Random()
  ' 选项之一:使用会话状态获得绘图列表
  '(保存在 Page_Unload 中)
  '(注意:要求服务器上的状态存储)
  drawingList = Session("drawingList")
  If drawingList Is Nothing Then drawingList = New DShapeList()
  ' 选择之二:从用户浏览器上的 cookie 中
  ' 检索绘图状态
  '(注意:不需要服务器存储,但有些用户会禁用 cookie)
  '(注意之二:cookie 不会自动反序列化!:( )
  ' 注意之三:使用 cookie 将限制能够绘制的形状数量
  'Dim drawingListCookie As HttpCookie
  'drawingListCookie = Request.Cookies("drawingList")
  'If drawingListCookie Is Nothing Then
  ' drawingList = New DShapeList()
  'Else
  ' drawingList = _
  ' SerialHelper.DeserializeFromBase64String( _
  ' drawingListCookie.Value)
  'End If
  End Sub
  
    首先,我们尝试从视图状态加载随机数发生器状态。如果存在,则使用存储的值。如果不存在,则创建一个新的 Random 对象。
  
    接下来,我们尝试从会话状态加载绘图列表。同样,如果不存在绘图列表,则创建一个新的空列表。
  
    如果需要,视图状态和会话状态都会自动序列化我们的对象。视图状态始终被序列化,因此可以表示为浏览器隐藏输入字段中的编码的字符串。会话状态当存储在数据库中或者在服务器间进行传递时被序列化,但是如果应用程序运行在单个服务器上(例如在开发机器上进行测试时),则不会将其序列化。
  
    被注释的代码试图从 cookie 加载绘图列表。请注意,处理 cookie 要比处理视图或会话状态复杂得多。首先就是不能自动序列化。为序列化为一个字符串,我们在一个新类当中编写了 helper 函数,如下所示:
  
  
  Public Shared Function DeserializeFromBase64String( _
  ByVal base64String As String) As Object
  Dim formatter As New BinaryFormatter()
  Dim bytes() As Byte = Convert.FromBase64String(base64String)
  Dim serialMemoryStream As New MemoryStream(bytes)
  Return formatter.Deserialize(serialMemoryStream)
  End Function
  
  
    Dr. GUI 使用了二进制格式化程序并转换为可打印的 base 64 字符串,因为无论是 SOAP 还是 XML 格式化程序都不适用于此应用程序。我们必须从纯二进制表示转换为 base 64 字符串,以避免因简单复制字节而产生字符串中控制字符的潜在问题。base 64 字符串使用一个字符 A-Z、a-z、0-9、+ 或 /(共 64 个或 2^6 个字符)来表示二进制字符串中的每六位,因此四个字符表示三个字节 -- 第一个字符表示第一个字节中的六位,第二个字符表示第一个字节的末两位和第二个字节的前四位,以此类推。同样,使用 base 64 字符串关键在于可以将字符串限制为可打印字符,这样就避免了任何控制字符出现潜在问题。
  
    XML 格式化程序不会序列化私有数据 -- 而 Dr. GUI 也不打算为绘图列表中的私有数据添加公开访问权限。SOAP 格式化程序不存在这种限制,但它不会序列化空列表以便进行反序列化。相反,它不为空列表向数据流写入任何东西,这样当尝试反序列化时就会引发一个异常。(Dr. GUI 认为这是一个错误。)
  
    Dr. GUI 更喜欢以可读的 XML 格式进行序列化,但由于两种 XML 序列化格式化程序都无法完成此项工作,所以最终选择了二进制格式化程序并转换为 base 64 字符串。
  
    Page_Unload
    Page_Unload 是在破坏页面对象(包括任何所包含的数据)之前被调用的,因此是永久放置重要数据的理想位置,这样我们便可以在将来从 Page_Load(或者从图像的 Page_Load)中取出这些数据。
  
    因此,我们将数据保存在 Page_Unload 中,并从 Page_Load 中检索数据。虽然这有些奇怪,但却是正确的。
  
    以下是 Page_Unload 的代码:
  
  Private Sub Page_Unload(ByVal sender As Object, _
  ByVal e As System.EventArgs) _
  Handles MyBase.PreRender
  ViewState("randomGen") = randomGen
  ' 选项之一:编写会话状态
  Session("drawingList") = drawingList
  
  ' 选项之二:编写一个 cookie。必须编写代码进行序列化。
  ' 注意:使用 cookie 将限制能够绘制的形状数量!
  'Dim drawingListString As String =
  ' SerialHelper.SerializeToBase64String(drawingList)
  'Response.Cookies.Add(New HttpCookie("drawingList", _
  ' drawingListString))
  End Sub
  
  
  
    此代码稍微有些简单,因为我们不必查看状态是否已经存在于视图或会话状态对象中,而只需将其无条件写出。
  
    同样,视图状态和会话状态可以自动对自身进行序列化,而 cookie 则不能,因此我们需要亲自执行。Dr. GUI 编写了下面的 helper 函数(在单独的类中),代码如下所示:
  
  
  Public Shared Function SerializeToBase64String(ByVal o As Object) _
  As String
  Dim formatter As New BinaryFormatter()
  Dim serialMemoryStream As New MemoryStream()
  formatter.Serialize(serialMemoryStream, o)
  Dim bytes() As Byte = serialMemoryStream.ToArray()
  Return Convert.ToBase64String(bytes)
  End Function
  
    在单独的页面中绘图
    正如前面提到的,绘图是在单独的页面中进行的。以下是该页面的代码:
  
  
  Private Sub Page_Load(ByVal sender As System.Object, _
  ByVal e As System.EventArgs) Handles MyBase.Load
  Dim drawingList As DShapeList
  ' 获取绘图列表选项之一:使用会话状态...
  drawingList = Session("drawingList")
  If drawingList Is Nothing Then drawingList = New DShapeList()
  
  ' 获取绘图列表选项之二:使用 cookie...
  '(请查看主页代码以了解更多注释)
  'Dim drawingListCookie As HttpCookie
  'drawingListCookie = Request.Cookies("drawingList")
  'If drawingListCookie Is Nothing Then
  'drawingList = New DShapeList()
  'Else
  ' drawingList = _
  ' SerialHelper.DeserializeFromBase64String( _
  ' drawingListCookie.Value)
  'End If
  
  Response.ContentType = "image/gif"
  Dim bitMap As New Bitmap(368, 376)
  Dim g As Graphics = Graphics.FromImage(bitMap)
  Try
  g.Clear(Color.White)
  drawingList.DrawList(g)
  bitMap.Save(Response.OutputStream, ImageFormat.Gif)
  Finally
  g.Dispose()
  bitMap.Dispose()
  End Try
  End Sub
  
  
    首先,我们从会话状态或 cookie 中获取绘图列表。(这部分代码与上面的 Page_Load 方法类似。)
  
    然后,我们将正在编写的响应流的 ContentType 设置为一个 GIF 图像。
  
    接下来,我们要做一些真正美妙的事情:按照所需的大小(本例按照与 Windows 窗体应用程序中相同的大小)创建一个位图。
  
    然后,我们得到一个与该位图相关联的 Graphics 对象,清除该对象,并在其中绘制我们的列表。
  
    下面的步骤很重要:接下来,我们将 GIF 格式的图像内容写出到响应流(即浏览器)中。我们设置了这种响应类型以确保浏览器能够正确解释图像,然后发送图像的位。(.NET Framework 使该操作变得相当简单。而在原来的 Windows GDI 时代,仅在位图上进行绘制都是非常痛苦的!)
  
    另一个重要步骤就是要记住清理 Graphics 和 Bitmap 对象 -- 并使用 Try/Finally,以便即使出现异常也会清理对象。
  
    嗨!步骤真多。但是为了让此应用程序能够作为 ASP.NET 应用程序运行,还是值得的 -- 并且更好的是,这种应用程序不需要依赖客户端脚本。
  
    试一试!
  
  
    如果您手头有 .NET,学习它的最好方法就是试一试。如果没有,就请想办法得到。如果您每周在 Dr. GUI .NET 上花费一个小时左右,那么在了解 .NET 之前您将已经成为一名 .NET Framework 专家了。
  
    从您开始 -- 并邀请您的朋友!
    作为第一个学习新技术的人,感觉一定不错,但如果和朋友们分享则乐趣更多!为享受更多乐趣,邀请朋友共同学习 .NET 吧!
  
    应进行的尝试...
    首先试一下这里给出的代码。其中有一些是从大型程序中节选下来的,围绕这些代码片断创建程序会取得不错的效果。(如果必须,也可以使用 Dr. GUI 提供的代码。)琢磨一下代码。
  
    向绘图程序添加一些不同的形状,包括填充和不填充的形状。注意:虽然我们没有对其进行转换,但所添加的类可能存在于来自其他文件的不同程序集中(因而是不同的可执行文件)。这意味着所添加的类的语言甚至可以和其他类不同。当您阅读并尝试有关的必要工作后,会更加确信这一点。
  
    请向这里的类添加一些方法,并尝试改动用户界面。自己制作一个可爱的 CAD 小程序。
  
    在自己选择的项目中使用继承、abstract/MustInherit 类、接口和多态。当类系列具有很多共同部分时,使用继承的效果最佳。如果类并不具有很多共同部分,但功能相似,这时使用接口的效果最好。
  
    尝试用 ASP.NET 编写自己的绘图应用程序。请注意,如果能够运行 .NET Framework,则只需 Microsoft Internet Information Server (IIS) 便可以在自己的计算机上运行 ASP.NET -- 无需服务器!Dr. GUI 认为,在便携式计算机上仅使用标准操作系统和免费的 .NET Framework 创建和测试 Web 应用程序,感觉实在好极了。(但 Dr. GUI 还是倾向于使用 Visual Studio,或者至少 Visual Basic 或 Microsoft Visual C#? 标准版。)看看吧!不需要服务器!甚至不需要 Internet 连接!(可在 Brand J 的扩展版上尝试…)

posted on 2004-10-07 14:25  TPoI  阅读(506)  评论(0)    收藏  举报

导航