Extending ASP.NET Webparts: Cross-browser Drag and Drop functionality using JQuery

One of the most interesting features in ASP.NET are WebParts. But one of the issues right now is the cross browser compatibility.

So with the default behavior you can add a WebpartZone and then drop your UserControl on it, but then even when you have a good usability experience using IE (you can drag & drop a WebPart to a different WebPartZone easily), as soon as you open it using Firefox or any other browser and activate the design mode of the page then you just lose this feature.

In order to achieve a cross browser experience in terms of drag and drop (which seems to be one of the most exciting feature when using webparts), I did some research and found nothing but some out-of-date workarounds to deal with this issue and partially.

So we decided to extend the WebPartZone class and add some JQuery capabilities in order to achieve the desired feature.

To solve the Drag & Drop problem of the WebParts we need three JQuery plugins.

  • UI Core
  • Draggable
  • Droppable

These plugins can be downloaded from http://www.jqueryui.com/download

The UI Core is required so the other two plugins can actually work.

Draggable plugin allows any element of the page to be dragged and droppable allows us to specify the areas were we can drop the selected element.

In order to add the drag behavior we need to add some code when the webpart is rendered. We can do this by extending the WebPartZone class.

In our example the page that inherits from WebPartZone is called ExtendedWebPartZone and it overrides the RenderBody method from the base class.

The following code can be used to extend the class:

   1:  
   2: protected bool IsDesignMode { get; set; }
   3: protected override void RenderBody(HtmlTextWriter writer)
   4: {
   5: if (this.WebPartManager.DisplayMode.Name == "Design")
   6: {
   7: this.IsDesignMode = true;
   8: }
   9: if (this != null)
  10: {
  11: if ((base.DesignMode
  12: || ((base.WebPartManager != null)
  13: && base.WebPartManager.DisplayMode.AllowPageDesign))
  14: && (((this.BorderColor != Color.Empty)
  15: || (this.BorderStyle != BorderStyle.NotSet))
  16: || (this.BorderWidth != Unit.Empty)))
  17: {
  18: Style style = new Style();
  19: style.BorderColor = this.BorderColor;
  20: style.BorderStyle = this.BorderStyle;
  21: style.BorderWidth = this.BorderWidth;
  22: style.AddAttributesToRender(writer, this);
  23: }
  24: writer.RenderBeginTag(HtmlTextWriterTag.Table);
  25: WebPartChrome webPartChrome = this.WebPartChrome;
  26: int index = 0;
  27: //If there are no webparts in this WebPartZone then
  28: //we render an empty droppable area
  29: if (WebParts.Count == 0 && this.IsDesignMode)
  30: {
  31: this.RenderEmptyZoneBody(writer, index);
  32: }
  33: else
  34: {
  35: //Loop through all the webparts and render the droppable areas
  36: //and add the draggable style to the webparts
  37: foreach (WebPart wp in WebParts)
  38: {
  39: if (this.IsDesignMode && index == 0)
  40: this.RenderDroppableRow(writer, index);
  41: writer.RenderBeginTag(HtmlTextWriterTag.Tr);
  42: writer.RenderBeginTag(HtmlTextWriterTag.Td);
  43: if (this.IsDesignMode)
  44: {
  45: writer.AddStyleAttribute(HtmlTextWriterStyle.Position, "");
  46: writer.AddAttribute(HtmlTextWriterAttribute.Class, "draggable");
  47: writer.AddAttribute("webPartID", "WebPart_" + wp.ID);
  48: writer.RenderBeginTag(HtmlTextWriterTag.Table);
  49: }
  50: else
  51: writer.RenderBeginTag(HtmlTextWriterTag.Table);
  52: writer.RenderBeginTag(HtmlTextWriterTag.Tr);
  53: writer.RenderBeginTag(HtmlTextWriterTag.Td);
  54: webPartChrome.RenderWebPart(writer, wp);
  55: writer.RenderEndTag();
  56: writer.RenderEndTag();
  57: writer.RenderEndTag();
  58: writer.RenderEndTag();
  59: writer.RenderEndTag();
  60: index++;
  61: if (this.IsDesignMode)
  62: this.RenderDroppableRow(writer, index);
  63: }
  64: }
  65: writer.RenderEndTag();
  66: }
  67: }
  68: //It renders a TableRow tag with the droppable attribute
  69: private void RenderDroppableRow(HtmlTextWriter writer, int index)
  70: {
  71: writer.RenderBeginTag(HtmlTextWriterTag.Tr);
  72: writer.AddAttribute(HtmlTextWriterAttribute.Class, "droppable");
  73: writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "20px");
  74: writer.AddAttribute("wpindex", index.ToString());
  75: writer.RenderBeginTag(HtmlTextWriterTag.Td);
  76: writer.RenderEndTag();
  77: writer.RenderEndTag();
  78: }
  79: //This method creates the empty zone that allows webparts
  80: //to be droped in
  81: private void RenderEmptyZoneBody(HtmlTextWriter writer, int index)
  82: {
  83: string emptyZoneText = this.EmptyZoneText;
  84: bool designMode = ((!base.DesignMode && this.AllowLayoutChange)
  85: && ((base.WebPartManager != null)
  86: && base.WebPartManager.DisplayMode.AllowPageDesign))
  87: && !string.IsNullOrEmpty(emptyZoneText);
  88: //Depending on the orientation that the webpart has is how 
  89: //we acctually render the empty zone
  90: if (this.LayoutOrientation == Orientation.Vertical)
  91: writer.RenderBeginTag(HtmlTextWriterTag.Tr);
  92: if (designMode)
  93: writer.AddAttribute(HtmlTextWriterAttribute.Valign, "top");
  94: if (this.LayoutOrientation == Orientation.Horizontal)
  95: writer.AddStyleAttribute(HtmlTextWriterStyle.Width, "100%");
  96: else
  97: writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "100%");
  98: writer.AddAttribute(HtmlTextWriterAttribute.Class, "droppable");
  99: writer.AddStyleAttribute(HtmlTextWriterStyle.Height, "20px");
 100: writer.AddAttribute("wpindex", index.ToString());
 101: writer.RenderBeginTag(HtmlTextWriterTag.Td);
 102: if (designMode)
 103: {
 104: Style emptyZoneTextStyle = base.EmptyZoneTextStyle;
 105: if (!emptyZoneTextStyle.IsEmpty)
 106: emptyZoneTextStyle.AddAttributesToRender(writer, this);
 107: writer.RenderBeginTag(HtmlTextWriterTag.Div);
 108: writer.Write(emptyZoneText);
 109: writer.RenderEndTag();
 110: }
 111: writer.RenderEndTag();
 112: if (this.LayoutOrientation == Orientation.Vertical)
 113: writer.RenderEndTag();
 114: if (designMode && this.DragDropEnabled)
 115: this.RenderDropCue(writer);
 116: }
 117:  
 118: protected override void Render(System.Web.UI.HtmlTextWriter writer)
 119:     {
 120:         if (this.WebPartManager.DisplayMode.Name == "Design")
 121:             this.IsDesignMode = true;
 122:  
 123:         if (this != null)
 124:         {
 125:             writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ID);
 126:             writer.AddAttribute(HtmlTextWriterAttribute.Class, "webPartZoneClass");
 127:             writer.RenderBeginTag(HtmlTextWriterTag.Table);
 128:             writer.RenderBeginTag(HtmlTextWriterTag.Tr);
 129:             writer.RenderBeginTag(HtmlTextWriterTag.Td);
 130:  
 131:             this.RenderBody(writer);
 132:  
 133:             writer.RenderEndTag();
 134:             writer.RenderEndTag();
 135:             writer.RenderEndTag();
 136:         }
 137:         else
 138:         {
 139:             base.RenderContents(writer);
 140:         }
 141:     }

The RenderBody method it's called every time a WebPartZone is rendered. We then iterate through the WebPart list to check if the WebPartManager is in DesignMode. If this is the case we check if this is the first WebPart in the list. If so, we call the RenderDroppableRow in order to inject a TableRow  into the HtmlTextWriter, to contain the droppable class and we set the attribute wpindex so we can use it at a later stage through our Jquery code to know where the webpart was dropped.

After that we need to check again the IsDesignMode flag, draw a table and set the CSS-class attribute to “draggable”. We also need to add the attribute webPartID so we can identify the webpart dropped and generate generate the correct postback in order to persist the changes.


Once we extended the class we need to modify the aspx page containing the WebParts.

First we need to add the references to the Jquery framework and the plugins.

<script src="Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>

<script src="Scripts/jquery-ui-1.7.2.custom.min.js" type="text/javascript"></script>

Next we can define some CSS style so when the DesignMode is activated the droppable areas are highlighted and when we move a webpart over a droppable zone it get's colored.

   1:  
   2: <style type="text/css">
   3: #PartZone
   4: {
   5: border: dashed 1px #DDDDDD;
   6: }
   7: .ui-state-hover
   8: {
   9: background-color: red;
  10: }
  11: .ui-state-active
  12: {
  13: border: dashed 1px red;
  14: }
  15: </style>

 

Finally we need to write some JQuery code:

   1:  
   2: <script type="text/javascript">
   3: $(document).ready(function() {
   4: //Here we set the parameters for the draggable webparts
   5: //(I.E: 
   6: $(".draggable").draggable({
   7: revert: 'invalid',
   8: zIndex: 1000
   9: });
  10: $(".droppable").droppable({
  11: tolerance: 'touch',
  12: activate: function(event, ui) {
  13: $(".droppable").addClass("ui-state-active");
  14: var parentTr = ui.draggable.parents("tr").eq(0);
  15: var prevDroppable = parentTr.prev().find(".droppable");
  16: var nextDroppable = parentTr.next().find(".droppable");
  17: prevDroppable.droppable('disable');
  18: nextDroppable.droppable('disable');
  19: prevDroppable.removeClass("ui-state-active");
  20: nextDroppable.removeClass("ui-state-active");
  21: },
  22: drop: function(event, ui) {
  23: var webPartZoneId = "ctl00:ContentPlaceHolder1:" + 
  24: $(this).parents(".webPartZoneClass").attr("ID");
  25: var index = $(this).attr("wpindex");
  26: var webPartId = ui.draggable.eq(0).attr("webPartID");
  27: __doPostBack(webPartZoneId, 'Drag:' + webPartId + ':' + index);
  28: },
  29: deactivate: function(event, ui) {
  30: $(".droppable").droppable('enable');
  31: $(".droppable").removeClass("ui-state-active");
  32: },
  33: over: function(event, ui) {
  34: $(this).removeClass("ui-state-active");
  35: $(this).addClass("ui-state-hover");
  36: },
  37: out: function(event, ui) {
  38: $(this).removeClass("ui-state-hover");
  39: $(this).addClass("ui-state-active");
  40: }
  41: });
  42: });
  43: </script>

 

Basically all this code is generic except when we create the variable webPartZoneId used to get something like this "ctl00:ContentPlaceHolder1:".

However we needed this because we are using a MasterPage, so prefix are needed for every control. This can be added dynamically.

The final step after you add the WebPartManager to the page is register the Assembly of the project that contains the ExtendedWebPartZone. For instance:

   1:  
   2: <%@ Register Assembly="UruIT.Web.UI.CustomWebPart" 
   3: Namespace="UruIT.Web.UI.CustomWebPart"
   4: TagPrefix="cc1" %>

 

 

We are now ready to add our webparts to the page! You can use some code similar to this one:

   1:  
   2: <table style="width: 100%;">
   3: <tr>
   4: <td>
   5: <cc1:ExtendedWebPartZone ID="LeftWebpartZone" runat="server" Width="100%" 
   6: BorderWidth="1px" MenuPopupStyle-BackColor="#E5EEFD" 
   7: MenuPopupStyle-Font-Size="10px">
   8: <ZoneTemplate>
   9: <uc2:Webpart1 ID="Webpart11" runat="server" />
  10: </ZoneTemplate>
  11: </cc1:ExtendedWebPartZone>
  12: </td>
  13: <td>
  14: <cc1:ExtendedWebPartZone ID="RightWebpartZone" runat="server" 
  15: Width="100%" BorderWidth="1px" MenuPopupStyle-BackColor="#E5EEFD" 
  16: MenuPopupStyle-Font-Size="10px">
  17: <ZoneTemplate>
  18: <uc3:Webpart2 ID="Webpart21" runat="server" />
  19: </ZoneTemplate>
  20: </cc1:ExtendedWebPartZone>
  21: </td>
  22: </tr>
  23: </table>

And this is how the drag and drop will look like in both Firefox and IE:

clip_image002

You could use the same idea to add more functionality to the webpart such a custom menu or any other cool visual behavior without needing a postback.

I’m planning to improve this code to get a more generic and reusable control. I’ll post it as soon as I’m done with it, but in the meantime this post can help anyone dealing with similar issues.

Post written by Sebastian Rodriguez – .NET Web Developer at UruIT - a Nearshore .NET Software Company

No Comments