Introduction
I have had several occasions where I needed an interface to create and order a list on the web. I had searched many times over the years looking for a clean and simple interface for doing this but never found one that I liked, so this is the solution that I created.
The ode
To see a demo of this, visit: http://www.wesgrant.com/SampleCode/SortableList/SortableListDemo.aspx.
The data model for this demo is a very simple master-detail relationship that has a list with list items. The model has lazy loading enabled, so I will introduce a detachable Data Repository so that the Entity Objects can be returned to jQuery AJAX calls as JSON.
The demo for this includes an interface with animated panels and the ability to create new lists, add, edit and delete items from the list, to preview the list with multiple stylesheet classes, and to change the user interface to use any of the jQueryUI themes. For the tutorial, I want to demonstrate passing JSON encoded Entity Framework objects through web methods using jQuery AJAX, so I will not cover many of the functions. Feel free to download the complete source code and have a look at the add, edit, and delete functions as well as using the jQueryUI show and hide functions to make the interface more interactive, which I will not cover in this tutorial.
Getting the Available Lists and Displaying them using jQuery AJAX and Web Methods
To get the lists that have been created, I will use a jQuery AJAX call to a WebMethod
named GetSortableLists
. I have included pageNumber
and pageSize
parameters that could be used to page the list but have not implemented paging in this code. These parameters are just there to show how data is passed to the web method. The success state calls the drawListofLists
function which I have not covered, but I do cover the drawing of the list items in the next section which is similar to this function.
function getLists() {
$.ajax({
type: "POST",
url: "AjaxMethods/ListMethods.aspx/GetSortableLists",
data: "{'pageNumber':'0','pageSize':'0'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
drawListofLists(data.d);
},
error: function (xhr, ajaxOptions, thrownError) {
showVoodooJavaScriptError(xhr, ajaxOptions, thrownError);
}
});
This AJAX call is to the following web method:
[System.Web.Services.WebMethod]
public static List<Data.Model.SortableList> GetSortableLists(int pageNumber, int pageSize)
{
Data.Repository.SortableListRepository SLR =
new Data.Repository.SortableListRepository();
return SLR.GetSortableLists(true);
}
Which is the bridge to the following repository method:
public Data.Model.SortableList GetSortableList(int sortableListId, bool detach = false)
{
var results = DataContext.SortableLists.Where
(l => l.SortableListId == sortableListId);
if (results.Count() > 0)
{
var result = results.First();
if (detach)
DataContext.Detach(result);
return result;
}
else
return null;
}
Notice that the repository method has an optional detachable parameter that is false
by default. This allows for the same repository class to return Entity
objects with their navigation properties as well as just the object which can be JSON encoded. If you try to return an Entity Framework object that has lazy loading enabled without detaching it and removing the navigation properties, you will receive the following error:
{"Message":"A circular reference was detected while serializing an object
of type \u0027DragAndDropSortableList.Data.Model.SortableList\u0027.", "StackTrace":"
at System.Web.Script.Serialization.JavaScriptSerializer.SerializeValueInternal(Object o,
StringBuilder sb, Int32 depth, Hashtable objectsInUse,
SerializationFormat serializationFormat)\r\n at
System.Web.Script.Serialization.JavaScriptSerializer.SerializeValue(Object o,
StringBuilder sb, Int32 depth, Hashtable objectsInUse, SerializationFormat
serializationFormat)\r\n...(PART OF THE STACK TRACE OMITED...)",
"ExceptionType":"System.InvalidOperationException"}
Drawing the Sortable List Items Interface and the List Preview with Various Stylesheets
Notice in the image below that as you drag the item over a position in the list, a red dotted box becomes visible to show that it can be dropped in that location.
To retrieve the data, we will make a simple AJAX call to our web method and receive a JSON encoded list of items back from the server.
function getListItems(sortableListId) {
$.ajax({
type: "POST",
url: "../AjaxMethods/ListMethods.aspx/GetSortableListItems",
data: "{'sortableListId':'" + sortableListId + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
drawListItems(data.d);
},
error: function (xhr, ajaxOptions, thrownError) {
showVoodooJavaScriptError(xhr, ajaxOptions, thrownError);
}
});
}
This is the web method used for the bridge between the AJAX call from jQuery and retrieving the data from the data repository. Don't forget, when creating Web Methods, the function must be marked as static
.
[System.Web.Services.WebMethod]
public static List<Data.Model.SortableListItem> GetSortableListItems(int sortableListId)
{
Data.Repository.SortableListRepository SLR =
new Data.Repository.SortableListRepository();
return SLR.GetSortableListItems(sortableListId);
}
Repository Function for Returning the Sortable List Items from the Data Model
Notice in the function below that the results are always detached from the data context. In the rest of the routines where there is a reason to have the entity attached and the navigation properties in place, detaching is an option. For this function, if you want the navigation properties in place, you can simply retrieve the list and use the SortableListItems
navigation property of the list.
public List<Data.Model.SortableListItem> GetSortableListItems(int sortableListId)
{
var results = DataContext.SortableListItems
.Where(i => i.SortableListId == sortableListId);
if (results.Count() > 0)
{
List<Model.SortableListItem> returnValue =
results.OrderBy(i => i.ItemOrder).ToList();
foreach (var item in returnValue)
DataContext.Detach(item);
return returnValue;
}
else
return new List<Model.SortableListItem>();
}
The success status of the AJAX call to get the list items calls the function drawListItems
and passes the returned JSON encoded list of items. This function creates the list of items for both moving the item position with drag and drop as well as previewing the list with various stylesheet classes. Here is the JavaScript code for drawing the list items. In the demo, there are buttons for editing and deleting the items as well as adding new items. I have left that code out of this for brevity.
function drawListItems(listItems) {
sortableList = "<div class=\"SortableList\">";
for (var index in listItems) {
sortableList += "<div class=\"SortableListItem\" id=\"" +
listItems[index].SortableListItemId + "\"
sortableList += style=\"margin-bottom: 5px;\">";
sortableList += "<div class=\"SortableListHeader\"
style=\"padding: 5px; position: relative; \">";
sortableList += listItems[index].Headline;
sortableList += "</div>";
sortableList += "<div class=\"SortableListContent\"
style=\"padding: 5px;\">";
sortableList += "Desc: " + listItems[index].Description + "<br />";
sortableList += "Link: " + listItems[index].LinkUrl + "<br />";
sortableList += "</div>";
sortableList += "</div>";
}
sortableList += '</div>';
previewList = "<div id=\"sidebar\">";
previewList += "<div id=\"listPreviewStyle\">";
previewList += "<ul>";
for (var indexP in listItems) {
previewList += " <li>";
previewList += " <a href='" + listItems[indexP].LinkUrl + "'
target='_blank'>"
previewList += listItems[indexP].Headline + "</a>";
previewList += "<span>" + listItems[indexP].Description + "</span>";
previewList += "</li>";
}
previewList += "</ul>";
previewList += "</div>";
previewList += "</div>";
$("#PreviewListContent").html(previewList);
$("#EditList").html(sortableList);
$(".listbutton").button();
$("#listPreviewStyle").addClass($('#uiCssClass option:selected').text());
var $StartIndex;
$(".SortableList").sortable({
start: function (event, ui) {
$StartIndex = ui.item.index() + 1;
},
stop: function (event, ui) {
idListItem = ui.item[0].id;
newListIndex = ui.item.index() + 1;
if ($StartIndex != newListIndex) {
moveListItem(idListItem, newListIndex);
}
}
});
$(".SortableListItem").addClass("ui-widget ui-widget-content ui-helper-clearfix
ui-corner-all")
.find(".SortableListHeader")
.addClass("ui-widget-header ui-corner-all")
.end()
.find(".SortableListContent");
$(".SortableList").disableSelection();
}
Here are the additional style classes that need to be added to the page for the function above. Notice the red dotted border, this is the style for the outline of the place where the item will be dropped.
<style type="text/css">
.ui-sortable-placeholder { border: 3pxdottedred;
visibility: visible!important;
height: 50px!important; }
.ui-sortable-placeholder* { visibility: hidden; }
</style>
The drawListItems
function above actually creates the following HTML output. First is the output for the sortable (left on above image) list edit interface and second is the output for the list preview (right on above image).
Sortable List output (left on image above):
<div class="SortableList">
<div class="SortableListItem"id="7" style="margin-bottom: 5px;">
<div class="SortableListHeader" style="padding: 5px; position: relative;">
Code Project
</div>
<div class="SortableListContent" style="padding: 5px;">
Desc: programming resource <br/>
Link: http://www.codeproject.com/ <br/>
</div>
</div>
<div class="SortableListItem"id="5" style="margin-bottom: 5px;">
<div class="SortableListHeader" style="padding: 5px; position: relative;">
wesgrant.com
</div>
<div class="SortableListContent" style="padding: 5px;">
Desc: my personal website <br/>
Link: http://www.wesgrant.com/ <br/>
</div>
</div>
</div>
List preview output (right on above image):
<div id="listPreviewStyle">
<ul>
<li>
<a href='http://www.codeproject.com/' target='_blank'>Code Project</a>
<span>programming resource</span>
</li>
<li>
<a href='http://www.wesgrant.com/' target='_blank'>wesgrant.com</a>
<span>my personal website</span>
</li>
</ul>
</div>
Notice in the code that sets up the sortable list, the “stop” state calls moveListItem
if the item position has changed. This code calls a web method to persist the move to the database and rearrange the list of items accordingly. This is the code for the moveListItem
function:
function moveListItem(sortableListItemId, newPosition) {
$.ajax({
type: "POST",
url: "../AjaxMethods/ListMethods.aspx/SaveListPosition",
data: "{'sortableListItemId':'" + sortableListItemId + "',"
+ "'newPosition':'" + newPosition + "'}",
contentType: "application/json; charset=utf-8",
dataType: "json",
success: function (data) {
},
error: function (xhr, ajaxOptions, thrownError) {
showVoodooJavaScriptError(xhr, ajaxOptions, thrownError);
}
});
}
The function passes the ID of the item being moved, as well as the new position that the item is to be dropped. This will be passed to a Web Method and the movement of the list items will ultimately occur in the data repository. This is the code for the intermediate Web Method:
[System.Web.Services.WebMethod]
public static void SaveListPosition(int sortableListItemId, int newPosition)
{
Data.Repository.SortableListRepository SLR = new
Data.Repository.SortableListRepository();
SLR.MoveSortableListItem(sortableListItemId, newPosition);
}
And finally the code that does all of the work. This retrieves the item from the Data Model so that it will have the current position of the item, as well as the ID of the list that the item belongs to. To perform the move, it moves all of the items in the list down that are above the item's current position, then after that move, moves all of the items back up one position that is greater than or equal to the new position, then sets the position of the item being moved to its new position.
public void MoveSortableListItem(int sortableListItemId, int newPosition)
{
var results = from i in DataContext.SortableListItems
where i.SortableListItemId == sortableListItemId
select i;
if (results.Count() > 0)
{
Data.Model.SortableListItem li = results.First();
var items = DataContext.SortableListItems
.Where(i => (i.SortableListId == li.SortableListId) &&
(i.ItemOrder > li.ItemOrder) &&
(i.SortableListItemId != sortableListItemId));
foreach (var item in items)
item.ItemOrder--;
DataContext.SaveChanges();
items = DataContext.SortableListItems
.Where(i => (i.SortableListId == li.SortableListId) &&
(i.ItemOrder >= newPosition) &&
(i.SortableListItemId != sortableListItemId));
foreach (var item in items)
item.ItemOrder--;
DataContext.SaveChanges();
items = DataContext.SortableListItems
.Where(i => (i.SortableListId == li.SortableListId) &&
(i.ItemOrder >= newPosition) &&
(i.SortableListItemId != sortableListItemId));
foreach (var item in items)
item.ItemOrder++;
li.ItemOrder = newPosition;
DataContext.SaveChanges();
foreach (var item in items)
item.ItemOrder++;
li.ItemOrder = newPosition;
DataContext.SaveChanges();
}
}
History
- 20th July, 2011: Initial post