65.9K
CodeProject 正在变化。 阅读更多。
Home

创建图像裁剪控件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (34投票s)

2008年7月9日

CPOL

6分钟阅读

viewsIcon

206770

downloadIcon

11216

创建一个自定义的 ASP.NET 3.5 控件,该控件使用 WebResources、客户端 JS 和 HTTPhandler。

Anders

引言

创建一个自定义的 ASP.NET Web 控件涉及很多不同的方面。控件的用户应该只需要将控件放置在页面上,之后控件就应该能自我维持。换句话说,它应该能自己注册客户端脚本、客户端 CSS 和客户端图像。

本文将指导您创建一个使用 Web 资源来管理脚本、CSS 和图像的自定义控件,并向您展示如何为页面上的每个控件初始化 JavaScript 代码。我们将涵盖使用实现 IHttpHandler 接口的类的基础知识。

使用代码

我们需要做的第一件事是创建所有的 Web 资源。我们将所有资源放在不同的文件夹中(cssjsimg)。选中每个文件夹中的内容,然后右键单击/属性。将所有这些文件的“生成操作”设置为“嵌入的资源”。

接下来是在 AssemblyInfo.cs 文件中注册这些资源。

//Cropping logic
[assembly: WebResource("Anders.Web.Controls.js.cropper.js", "text/javascript")]

//Script lib
[assembly: WebResource("Anders.Web.Controls.js.lib.builder.js", "text/javascript")]
[assembly: WebResource("Anders.Web.Controls.js.lib.dragdrop.js", "text/javascript")]
[assembly: WebResource("Anders.Web.Controls.js.lib.effects.js", "text/javascript")]
[assembly: WebResource("Anders.Web.Controls.js.lib.prototype.js", "text/javascript")]

[assembly: WebResource("Anders.Web.Controls.css.cropper.css", 
                       "text/css", PerformSubstitution = true)]

[assembly: WebResource("Anders.Web.Controls.img.marqueeHoriz.gif", "image/gif")]
[assembly: WebResource("Anders.Web.Controls.img.marqueeVert.gif", "image/gif")]

请注意,我们为 CSS 资源使用了 PerformSubstitution 属性。ASP.NET 引擎会使用这个属性来为 CSS 中定义的背景图片呈现正确的路径。

background: transparent url('<%=WebResource(
            "Anders.Web.Controls.img.marqueeHoriz.gif")%>') repeat-x 0 0;

下一步是创建裁剪器控件类,并让它继承自 CompositeControl。这是一个功能强大的抽象类,允许您在自定义控件中重用 .NET 控件。

我还继承了 IPostBackDataHandler 接口;这样我就可以将该控件注册为回发处理程序。

让我们添加构成我们控件的所有不同子控件。

private Image image = new Image();
private HiddenField cropCords = new HiddenField();
private CropAreaCordinates cropArea = new CropAreaCordinates();

protected override void CreateChildControls()
{
    EnsureChildControls();
    CheckForHandler();
    
    image.ID = "cropImage";
    cropCords.ID = "cords";

    Controls.Add(cropCords);
    Controls.Add(image);

    base.CreateChildControls();
}

图像将是裁剪器的主要用户界面,而隐藏字段将存储所有的裁剪坐标,以便用户可以将它们回传给控件。

我们还需要设置子控件的 ID,因为这些 ID 不会自动生成。这些 ID 将继承父控件的 ID 作为前缀。

我们的控件实现了 IPostBackDataHandler 接口,这强制我们实现 LoadPostData 方法。

public bool LoadPostData(string postDataKey, 
       System.Collections.Specialized.NameValueCollection postCollection)
{
    string v = postCollection[postDataKey + "$cords"];
    if (!string.IsNullOrEmpty(v))
    {
        string[] values = v.Split(';');
        cropArea.X = int.Parse(values[0]);
        cropArea.Y = int.Parse(values[1]);
        cropArea.Width = int.Parse(values[2]);
        cropArea.Height = int.Parse(values[3]);

        //This values are not saved in client hiddenfield, 
        //we retrive them from viewstate instead
        cropArea.MinWidth = MinWidth;
        cropArea.MinHeight = MinHeight;
        return true;
    }
    else
        return false;
}

我们只解析隐藏字段的值,这些值稍后会用到。

现在,是时候初始化客户端脚本和属性了。这在 OnPrerender 方法中完成。

protected override void OnPreRender(EventArgs e)
{
    if (CropEnabled)
    {
        InitClientCrop();
        InitImages();

        float h = (float)CroppedImageHeight / (float)CroppedImageWidth;
        IFormatProvider culture = new CultureInfo("en-US", true);
        string height = h.ToString(culture);

        image.Attributes["onload"] = string.Format("InitCrop(this.id, 
              {0},{1},{2},{3},{4},{5}, '{6}', {7}, {8}, {9});",
            AllowQualityLoss ? 0 : cropArea.MinWidth,
            AllowQualityLoss ? 0 : cropArea.MinHeight,
            cropArea.X,
            cropArea.Y,
            cropArea.Width,
            cropArea.Height,
            cropCords.ClientID,
            CaptureKeys.ToString().ToLower(),
            MaintainAspectRatio ? 1 : 0,
            MaintainAspectRatio ? height : "0"
        );                
        image.ImageUrl = string.Format("{0}?cropCacheId={1}", httpHandlerPath, CacheKey);
        cropCords.Value = string.Format("{0};{1};{2};{3}", cropArea.X, 
                          cropArea.Y, cropArea.Width, cropArea.Height);
        Page.RegisterRequiresPostBack(this);
    }
    else
        image.Attributes.Remove("onload");
    base.OnPreRender(e);
}

首先,我们在 InitClientCrop 方法中初始化客户端脚本(稍后我将详细介绍),然后,在 InitImages 方法中初始化将要显示给用户的图像。

我们希望这个控件在同一个页面上可以有多个实例正常工作。为了实现这一点,我们需要为每个控件触发初始化脚本。这是通过图像元素的 onload 客户端事件来完成的。当 Web 浏览器加载完图像后,onload 事件就会触发。

初始化脚本将实例化裁剪器的 JavaScript 类,并为其提供所有属性。

请注意,我们还设置了图像的路径,并在路径中提供了缓存 ID,我们稍后会回到这一点。

请记住,该类实现了 IPostBackDataHandler 接口,但我们还需要让页面知道我们对回发数据感兴趣。这是通过 Page.RegisterRequiresPostBack 方法完成的。

脚本资源的注册过程是这样的:首先,我们像这样获取资源的相对路径:

string protoTypePath = this.Page.ClientScript.GetWebResourceUrl(typeof(ImageCropper),
                       "Anders.Web.Controls.js.lib.prototype.js");

然后,我们使用 ClientScriptManager 类的 RegisterClientScriptInclude 方法在页面上注册它。

this.Page.ClientScript.RegisterClientScriptInclude("prototype.js", protoTypePath);

我们还需要在页面上注册 CSS 资源。没有内置的方法可以实现这一点,所以需要一些额外的工作。

if (Page.Header.FindControl("cropCss") == null)
{
    HtmlGenericControl cssMetaData = new HtmlGenericControl("link");
    cssMetaData.ID = "cropCss";
    cssMetaData.Attributes.Add("rel", "stylesheet");
    cssMetaData.Attributes.Add("href",
        Page.ClientScript.GetWebResourceUrl(typeof(ImageCropper),
        "Anders.Web.Controls.css.cropper.css"));
    cssMetaData.Attributes.Add("type", "text/css");
    cssMetaData.Attributes.Add("media", "screen");
    Page.Header.Controls.Add(cssMetaData);
}

由于页面上可以有多个控件,我们需要检查是否已有其他控件注册了 CSS。这可以通过 FindControl 方法来完成。如果 FindControl 方法返回 null,我们就将 CSS 控件添加到页面的 head 控件中。

脚本注册完成后,我们需要渲染将呈现给用户的客户端图像。

private void CreateImage()
{
    ImageManager imageManager = new ImageManager();
    CropData cropData = imageManager.GetImageCropperDisplayImage(CroppedImageWidth, 
                        CroppedImageHeight, SourceImage, JpegQuality);
    cropArea = cropData.CropAreaCordinates;
    //Min width and min height is not sent to client's hiddenfield 
    //so we need to save this in viewstate.
    MinWidth = cropArea.MinWidth;
    MinHeight = cropArea.MinHeight;

    //Saves buffer to cache
    SetBuffer(string.Empty, cropData.Buffer);
}

我不会详细介绍图像是如何处理的,如果您对我如何处理图像感兴趣,可以查看应用程序层(或者如果您喜欢这个术语,也可以叫业务逻辑层)中的 ImageManager。上述方法首先请求图像管理器创建一个供用户显示的图像。该方法将控件的一些属性作为参数,CroppedImageWidth/height 告诉逻辑期望的最终宽度/高度。SourceImage 包含由控件用户设置的源图像字节缓冲区。JpegQuality 属性定义了 JPEG 应该以何种质量进行渲染。此方法还计算了裁剪区域的默认值。

最后但同样重要的是,我们将缓冲区保存到缓存中。缓存代码是这个控件中一个相当大的部分,但我不会深入细节。您只需要知道,我在控件的视图状态中存储了一个 GUID,并使用这个 GUID 作为缓存键。

应用程序层还负责处理裁剪图像的逻辑。

那么现在,我们已经注册了所有的脚本和资源,也创建了一个服务器端的图像。但是,我们如何将图像呈现给客户端呢?我们可以让控件的用户创建一个会返回图像的 ASPX 页面。但是,请记住,这个控件的设计理念是,我们只希望用户将一个控件拖到页面上,然后它就应该自己完成所有的神奇工作。强制用户创建一个 ASPX 页面并不符合这个理念。

因此,解决方案是 IHttpHandler 接口。

您可以注册任何 HTTPHandler,并在 web.config 文件中将其引用到一个路径。

<add path="CropImage.axd" verb="*" 
  type="Anders.Web.Controls.ImageCropperHttpHandler" validate="false" />

当 ASP.NET 收到对特定路径(在本例中是 “CropImage.axd”)的请求时,它将实例化在 type 属性中定义的类。

HTTPHandler 类的唯一要求是它必须实现 IHttpHandler 接口。

public class ImageCropperHttpHandler : IHttpHandler
{
    public bool IsReusable
    {
        get { return true; }
    }

    public void ProcessRequest(HttpContext context)
    {
        string cacheId = context.Request.QueryString["cropCacheId"];
        byte[] buffer = context.Cache[cacheId] as byte[];
        context.Response.ContentType = "image/jpeg";
        context.Response.OutputStream.Write(buffer, 0, buffer.Length);
        
    }
}

IsReusable 属性告诉 ASP 引擎是否可以重用处理程序的实例,或者是否需要为每个对该类的请求重新创建实例。在这种情况下,完全可以重用它,因为它是无状态的。

当 ASP 引擎收到请求时,会调用 ProcessRequest 方法。它提供了当前的 HTTP 上下文,这就是您获取当前 ASP.NET 作用域所需的一切。

这个方法所做的全部工作就是解析 cropCacheId 查询字符串,并使用输出流输出图像。

使用 HTTPHandler 的唯一缺点是,它们要求您在 web.config 中注册数据。因此,我们添加一个方法来验证控件的用户是否已经这样做了。

private void CheckForHandler()
{
    if (httpHandlerPath == null)
    {
        HttpHandlersSection handlerSection = 
          WebConfigurationManager.GetWebApplicationSection("system.web/httpHandlers") 
          as HttpHandlersSection;
        bool foundHandler = false;
        Type type = typeof(ImageCropperHttpHandler);
        string handlerName = type.ToString();
        string fullHandlerName = type.AssemblyQualifiedName;
        foreach (HttpHandlerAction action in handlerSection.Handlers)
            if (action.Type == handlerName || action.Type == fullHandlerName)
            {
                foundHandler = true;
                httpHandlerPath = action.Path.StartsWith("~") ? 
                                  string.Empty : "~/" + action.Path;
                break;
            }

        if (!foundHandler)
            throw new ApplicationException(string.Format("The HttpHandler {0} is" + 
                      " not registered in the web.config", handlerName));
    }
}

此方法从 CreateChildControls 方法中调用,并且在每个应用程序作用域中只调用一次。它检查 web.configHttpHandlers 部分;如果其中包含正确的类型,我们就解析路径并将其用作图像路径。如果它不存在,我们就抛出一个 ApplicationException 异常。

最后一点说明,此控件的所有属性都使用视图状态。我见过很多文章中人们使用标准属性。如果您在每次加载页面时都设置属性,这可能行得通。但是,标准的 .NET 控件并不是这样工作的。您应该始终使用视图状态来实现属性,不应由您来决定控件的用户是否喜欢视图状态。如果他/她不想使用视图状态,只需关闭它即可。

历史

  • 版本 1。
  • 版本 1.1(添加了 IPostBackDataHandler 接口,并为 HttpHandler 在 web.config 中的注册添加了验证)。
  • 修复了一个小错误(上传新图片时控件没有刷新)。
© . All rights reserved.