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:
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