Tuesday, July 21, 2009

Widgets: Drag, drop and reorder. Simply done with JQUERY.

The heart of the Dropthings project, IGoogle or Pageflakes is the client side moving of the widgets. Be it dragging and dropping the widgets in other columns or reordering the widgets in a single column, this client side behaviour it the flashy part of the widget system.

In the Dropthings project the Author originally used the microsoft AJAX client side library along with quite a bit of his own code. No critisim here, he coded a complex cross-browser system that WORKED! I would not even have tried.

But, in the latest addition it says that the code was converted over to JQUERY. Well I looked at that code and while he may have converted to use JQUERY, he is not really USING JQUERY. Most of his system is still in place. No problem with that, unless you are not the Author and want to modify or enhance it.

I went another route: pure JQUERY. With just a few lines of code and a bit of AJAX to update the server I got all that functionality without all the complexity. Here it is:

        $(document).ready(function() {
$(".DropZone").sortable({
opacity: 0.5,
smooth: true,
helper: "clone",
handle: ".WidgetHeader",
connectWith: ".DropZone",
placeholder: "WidgetSortPlaceholderHighlight",
forcePlaceholderSize: true,
stop: function(event, ui) {
callMyService();
}
});
});


Thats it. You now have multi column drag and drop support. Just have your div's associated with the DropZone class and the Header of the WidgetContainer associated with the WidgetHeader class. My OnStop call to callMyService() walks through all the .DropZone controls and gets the ID of the Widget, the ID of the DropZone and the index (the order of the widgets) and calls the Webservice to update the database.

Here is a link to a working minimum example.

15 comments:

Kalitte said...

Also Dynamic Dashboard & Widget Toolkit for Asp.Net. Drag-drop widgets without thinking about viewstate.

Visit Widget Toolkit for Asp.net to get more information.

Commercial, reliable and professional than dropthings.

fxmisticat said...

Do you have a sample project to show the whole thing in action?

Kalitte said...

Sure. Official web site is a sample. It uses SessionDashboardProvider and open-source. Visit www.dynamicdashboards.net.

Lee Saunders said...

On the off chance you were asking me :-) if I had a working example of the code in my post, I've added a link to a working, 3 column example.

Rickly H said...

Lee,
what would the callMyService();
function typically look like?

I want to call an aspx page in the background which handles the updates.

I guess I woud want to send the widgetID, column and position.

Or the position of all the widgets in the updated column?

Lee Saunders said...

Rickly,

Here is the callMyService() that my team came up with. We desided to go with one call per widget:

function callMyService() {
$(".DropZone").children().each(function(index) {
var WidgetId = this.id;
var PanelId = $(this).parent('div:first').attr("id");
var WidgetOrderNo = index;

$.ajax({
type: "POST",
url: "WebServices/WidgetServices.asmx/UpdateWidgetLocations",
data: "{ 'PanelId' : '" + PanelId + "' , 'WidgetId' : '" + WidgetId + "', 'WidgetOrderNo' : '" + WidgetOrderNo + "' }",
contentType: "application/json; charset=utf-8",
dataType: "json"
});
});
}

Rickly H said...

How will my webmethod look?

[WebMethod]
public string UpdateWidgetPosition()
{
//update the table
return "Thanks, position updated";
}

Do I put the variables between the brackets ()

or is there a different way to receive the json?

or is there an alternative to sending Json to the webmethod (sorry, totally new to the Ajax stuff..)

Lee Saunders said...

Here is our webservice:

[WebService(Namespace = "http://tempuri.org/")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
// To allow this Web Service to be called from script, using ASP.NET AJAX, uncomment the following line.
[ScriptService]
public class WidgetServices : WebService
{
[WebMethod]
public void UpdateWidgetLocations(string PanelId, string WidgetId, int WidgetOrderNo)
{
var Panels = new[] {"LeftPanel", "MiddlePanel", "RightPanel"};

var Parameters = new SqlParameter[3];
Parameters[0] = (new SqlParameter("@ColumnNo", Array.IndexOf(Panels, PanelId)));
Parameters[1] = (new SqlParameter("@OrderNo", WidgetOrderNo));
Parameters[2] = (new SqlParameter("@WidgetInstanceId", int.Parse(WidgetId.Split('_')[0])));

BaseData.ExecuteNonQuery("dbo.UpdateWidgetInstanceLocation", Parameters);
}
}

Rickly H said...

Great thanks
I'll let you know how we get on

Rickly H said...

I have this in the webservice:

[WebMethod]
public string UpdateWidgetPosition(string PanelId, string WidgetID, int WidgetOrderNo, int AgentUserID)
{

..etc


and this is the jquery:
function callMyService() {
$(".DropZone").children().each(function(index) {
var WidgetId = this.id;
var PanelId = $(this).parent('div:first').attr("id");
var WidgetOrderNo = index;

$.ajax({
type: "POST",
url: "/Ajax/WidgetUpdater.asmx/UpdateWidgetPosition",
data: "{ 'PanelId' : '" + PanelId + "' , 'WidgetId' : '" + WidgetId + "', 'WidgetOrderNo' : '" + WidgetOrderNo + "', 'AgentUserID' : '117' }",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: alert("called my service - success, WidgetID: "+ WidgetId + ", PanelId: " + PanelId + ", Widget Order No.: "+ WidgetOrderNo)
});

});
alert("called my service - finished");
}

I get all the alerts to say it's working ok, don't get any errors but nothing happens.

Are you sure it can translate the json into a list of variables for the c#

Thanks

Lee Saunders said...

Sorry to here about your troubles. It works in my project.

If I have time, I'll strip down a Visual Studio project that has the needed WebService.

Alexis said...

Hey man, this code is so good and now I'm able to store the relation widget, panel and order in my database because of you, but now, how can I do to order my widgets in the panel related in my database the next time when I'll enter to my page? Thanks a lot man

Lee Saunders said...

Alexis,

First of all, I am assuming, that like me, you are loading your Widgets dynamically with LoadContol.

The trick is to write the SQL to get the list of Widgets that you need to load into your Panels ordered by their rank. Then as each one is loaded with LoadControl, just put it in the correct Panel. When done, they will be in the same order you stored them in.

Alexis said...

Ok man thanks for your reply, so, do you have an example that help me for doing that? I'm newer with jquery and this web problems and I'll really apreciate your help, Thank You friend

Lee Saunders said...

OK, so assuming your widget columns are asp:panel controls, then here is the code that loads the widgets:

internal void LoadWidgets()
{
Panel[] PanelArray = new[] {LeftPanel, MiddlePanel, RightPanel};

var Parameters = new SqlParameter[2];
Parameters[0] = (new SqlParameter("@LoweredUserName", CurrentUser.UserName.ToLower()));
Parameters[1] = (new SqlParameter("@Title", CurrentPage.Title));

DataSet ds = BaseData.GetDataSet("dbo.GetWidgetInstances", Parameters);

foreach (DataRow dr in ds.Tables[0].Rows)
{
Control MyControl = LoadControl("WidgetContainer.ascx");
if (MyControl is IWidgetHost)
{
MyControl.ID = dr["Id"].ToString();
(MyControl as IWidgetHost).WidgetIsExpandable = (bool)dr["Expandable"];
(MyControl as IWidgetHost).WidgetIsExpanded = (bool)dr["Expanded"];
(MyControl as IWidgetHost).WidgetTitleText = dr["Title"].ToString();
(MyControl as IWidgetHost).WidgetUrl = dr["Url"].ToString();
(MyControl as IWidgetHost).WidgetData = dr["State"].ToString();
(MyControl as IWidgetHost).WidgetIsClosable =(bool)dr["Closable"];
(MyControl as IWidgetHost).WidgetDisplayEdit = (bool)dr["DisplayEdit"];
(MyControl as IWidgetHost).WidgetColumnNo = (int)dr["ColumnNo"];
}

PanelArray[(int) dr["ColumnNo"]].Controls.Add(MyControl);
}
}

And inside the WidgetContainer:

protected override void OnInit(EventArgs e)
{
base.OnInit(e);

var widget = LoadControl(WidgetUrl);
if (widget is IWidget) (widget as IWidget).Container = this;
if (widget is IWidget) (widget as IWidget).SetData(WidgetData);
WidgetBodyPanel.Controls.Add(widget);

ContainedControl = (widget as IWidget);
if (ContainedControl != null) ContainedControl.Initialize();
}

Hope this helps!