创建图像裁剪控件






4.93/5 (34投票s)
创建一个自定义的 ASP.NET 3.5 控件,该控件使用 WebResources、客户端 JS 和 HTTPhandler。
引言
创建一个自定义的 ASP.NET Web 控件涉及很多不同的方面。控件的用户应该只需要将控件放置在页面上,之后控件就应该能自我维持。换句话说,它应该能自己注册客户端脚本、客户端 CSS 和客户端图像。
本文将指导您创建一个使用 Web 资源来管理脚本、CSS 和图像的自定义控件,并向您展示如何为页面上的每个控件初始化 JavaScript 代码。我们将涵盖使用实现 IHttpHandler
接口的类的基础知识。
使用代码
我们需要做的第一件事是创建所有的 Web 资源。我们将所有资源放在不同的文件夹中(css、js、img)。选中每个文件夹中的内容,然后右键单击/属性。将所有这些文件的“生成操作”设置为“嵌入的资源”。
接下来是在 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.config 的 HttpHandlers
部分;如果其中包含正确的类型,我们就解析路径并将其用作图像路径。如果它不存在,我们就抛出一个 ApplicationException
异常。
最后一点说明,此控件的所有属性都使用视图状态。我见过很多文章中人们使用标准属性。如果您在每次加载页面时都设置属性,这可能行得通。但是,标准的 .NET 控件并不是这样工作的。您应该始终使用视图状态来实现属性,不应由您来决定控件的用户是否喜欢视图状态。如果他/她不想使用视图状态,只需关闭它即可。
历史
- 版本 1。
- 版本 1.1(添加了
IPostBackDataHandler
接口,并为 HttpHandler 在 web.config 中的注册添加了验证)。 - 修复了一个小错误(上传新图片时控件没有刷新)。