Click here to Skip to main content
15,881,248 members
Articles / Web Development / ASP.NET
Article

Create an image cropping control

Rate me:
Please Sign up or sign in to vote.
4.93/5 (38 votes)
8 Jul 2008CPOL6 min read 197.9K   11.2K   119   62
Create a custom ASP.NET 3.5 control with WebResources, client-side JS, and HTTPhandlers.

Anders

Introduction

There's a lot of different aspects in creating a custom ASP.NET web control. The user of the control should only need to place the control on the page, and after that the control should be self sustained. In other words, register client-side scripts, client-side CSS, and client-side images.

This article will guide you in creating a custom control that uses web resources for scripts, CSS, and images, and show you how to init JavaScript code for each control on the page. We'll cover the basics of using classes that implements the IHttpHandler interface.

Using the code

The first thing that we need to do is create all the web resources. We place all the resources in different folders (css, js, img). Select the content in each folder and right click / Properties. Set the build action to Embedded Resource for all of these files.

The next thing is to register the resources in the AssemblyInfo.cs:

C#
//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")]

Note that we have used the PerformSubstitution attribute for the CSS resource. This is used by the ASP.NET engine to render the correct path for the background images defined in the CSS.

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

The next step is to create the cropper control class and let it inherit from CompositeControl. This is a powerful abstract class that lets you reuse .NET controls in a custom control.

I also inherit from IPostBackDataHandler; this way I can register the control as a postback handler.

Let’s add all the different controls that will be our control.

C#
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();
}

The image will be the main user interface for the cropper, and the hidden field will store all of the cropping coordinates so that the user can postback them right back to the control.

We also need to set the IDs of the child controls since these will not get auto-generated. The IDs will inherit the parent ID as prefix.

Our control implements the IPostBackDataHandler interface, it forces us to implement the LoadPostData method.

C#
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;
}

We only parse the hidden field values that will be used later on.

Now, it’s time for the initialization of the client-side scripts and properties. This is done in the OnPrerender method.

C#
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);
}

First, we init the client-side scripts in the InitClientCrop method (I will cover this more), then we init the image that will be displayed for the user, in the InitImages method.

We want this control to work with several instances on the same page. To make this happen, we need to fire the init script for each control. This is done using the onload client-side event for the image element. The onload event will fire once the web browser has finished loading the image.

The init script will instantiate the cropper JavaScript class and give it all its properties.

Note that we also set the path of the image and that we supply the cache ID in the path, we’ll get back to this.

Remember that the class implements IPostBackDataHandler, but we also need to let the page know that we're interested in the postback data. This is done using the Page.RegisterRequiresPostBack method.

The script resource register process is like this: first, we get the relative path to the resource like this:

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

Then, we register it on the page using the RegisterClientScriptInclude method of the ClientScriptManager class:

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

We also need to register the CSS resource on the page. There is no built-in method for this, so it’s a bit of plumbing.

C#
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);
}

Since there can be more then one control, we need to check if any other control already has registered the CSS. This is done using the FindControl method. We then add the CSS control to the head control of the page if the FindControl method returns null.

After the scripts have been registered, we need to render the client-side image that will be presented to the user.

C#
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);
}

I will not go into the details on how the images are handled, you can check out the ImageManager in the Application layer (or Business logic layer if you like that term better) if you're interested in how I process the image. The above method first asks the image manager to create an image to display for the user. This method takes some of the control's properties as parameters, the CroppedImageWidth/height tells the logic what the desired resulting width / height should be. SourceImage contains the byte buffer source image set by the user of the control. The JpegQuality property defines which quality the JPEG should be rendered in. This method also calculates the default values for the cropping area.

Last but not least, we save the buffer to the cache. The cache code is a pretty big part of this control, but I will not go into details. All you need to know is that I store a GUID in the control’s view state and that I use this GUID as a cache key.

The Application layer also takes care of the logic for cropping the image.

So now, we have registered all the scripts and resources, we have also created a server-side image. But, how do we present the image to the client? We could let the user of the control create an ASPX page that will return an image. But, remember that the idea of this control was that we only wanted the user to drag one control onto the page and it should do all the magic itself? To force the user to create an ASPX page does not match that profile.

So, the solution is the IHttpHandler interface.

You can register any HTTPHandler and reference it to a path in the web.config.

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

When ASP.NET gets a request for the specific path, in this case, "CropImage.axd", it will instantiate the class defined in the type attribute.

The only requirement of a HTTPHandler class is that it implements the IHttpHandler interface.

C#
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);
        
    }
}

The IsReusable property tells the ASP engine if it can reuse an instance of the handler, or if it needs to recreate the instance for each request to the class. In this case, it's perfectly OK to reuse it since it’s stateless.

The ProcessRequest method is called when the ASP engine gets a request. It supplies the current HTTP context, and it's all you need to get hold of the current ASP.NET scope.

All this method does is to parse the cropCacheId query string and to output the image using the output stream.

The only downside of using HTTPHandlers is that they require you to register data in the web.config. So we add a method to validate that the user of the control has done just that.

C#
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));
    }
}

This method is called from the CreateChildControls method, and is called only once per application scope. It checks the HttpHandlers section of the web.config; if it contains the correct type, we parse the path and uses that as the image path. If it does not exist, we throw an ApplicationException.

One last note, all the properties for this control uses the view state. I’ve seen lots of articles where people have used standard properties. This may work if you set the properties each time you load the page. But, this is not how the standard .NET controls work. You should always implement properties using the view state, it’s not up to you to decide if a user of your control likes or dislikes view state. If he/she does not want to use view state, it’s just a matter of turning it off.

History

  • Version 1.
  • Version 1.1 (added IPostBackDataHandler interface and added validation for the HttpHandler web.config registration).
  • Fixed minor bug (uploading of new image did not refresh the control).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Agero
Sweden Sweden
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionexcellent Pin
pradiptachaudhuri23-Sep-14 4:10
pradiptachaudhuri23-Sep-14 4:10 
BugLoadPostData crash Pin
anarfox1-Apr-13 6:30
anarfox1-Apr-13 6:30 
QuestionHow to save image to server.... Pin
Sahil Mehta6-Nov-12 21:39
Sahil Mehta6-Nov-12 21:39 
AnswerRe: How to save image to server.... Pin
Malek Chtiwi15-Nov-12 22:00
Malek Chtiwi15-Nov-12 22:00 
BugIssue IE 8 Pin
Dark Project5-Nov-12 23:32
Dark Project5-Nov-12 23:32 
Questionhey Pin
Member 936412617-Aug-12 23:51
Member 936412617-Aug-12 23:51 
QuestionImage Cropper with a State Server Pin
Member 93077705-Aug-12 19:33
Member 93077705-Aug-12 19:33 
QuestionMultiple croppers for a single image Pin
Noman Aftab7-Jul-12 19:25
Noman Aftab7-Jul-12 19:25 
SuggestionjQuery Pin
anarfox18-May-12 7:33
anarfox18-May-12 7:33 
GeneralMy vote of 5 Pin
Member 1051082230-Mar-12 8:08
professionalMember 1051082230-Mar-12 8:08 
QuestionAnders:ImageCropper control Pin
joti parkash16-Oct-11 2:25
joti parkash16-Oct-11 2:25 
QuestionAnders:ImageCropper control Pin
joti parkash16-Oct-11 1:17
joti parkash16-Oct-11 1:17 
Question[My vote of 2] Anders:ImageCropper control Pin
joti parkash15-Oct-11 22:02
joti parkash15-Oct-11 22:02 
QuestionAnders:ImageCropper control Pin
joti parkash15-Oct-11 22:02
joti parkash15-Oct-11 22:02 
Questionimage cropping problem not working live server Pin
AlokSingh6814-Aug-11 21:44
AlokSingh6814-Aug-11 21:44 
GeneralNot working in .aspx Pin
anjali24-May-11 23:21
anjali24-May-11 23:21 
Generalit is not working on my live site Pin
Nilesh Ahir7-Jul-10 1:12
Nilesh Ahir7-Jul-10 1:12 
GeneralRe: it is not working on my live site Pin
Anders Malmen15-Jul-10 1:05
Anders Malmen15-Jul-10 1:05 
GeneralRe: it is not working on my live site Pin
ravindranathreddy5-Aug-11 1:30
ravindranathreddy5-Aug-11 1:30 
GeneralRe: it is not working on my live site Pin
Member 930777025-Jul-12 18:06
Member 930777025-Jul-12 18:06 
Generalthis control is WORST Pin
shubham.mvp26-Apr-10 4:08
shubham.mvp26-Apr-10 4:08 
GeneralMy vote of 1 Pin
Raxita Chudasama19-Apr-10 23:28
Raxita Chudasama19-Apr-10 23:28 
GeneralMy vote of 5 Pin
Yves7-Apr-10 13:58
Yves7-Apr-10 13:58 
QuestionNeed to Add gridline in the Crop Area Pin
rajan418@hotmail.com28-Jan-10 23:51
rajan418@hotmail.com28-Jan-10 23:51 
Hi Anders Malmen,

This Control is very very helpful for me and I have one request, in the crop area I want to add Gridline for the purpose of finding the center point of the image so please tell me where do i have to add the gridline in this code .

Thanks,
Regards,

Rajan.
Generalthe crop section is not working Pin
Suman_MFST3-Dec-09 1:54
Suman_MFST3-Dec-09 1:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.