Click here to Skip to main content
Click here to Skip to main content

Drag and Drop Sortable Lists using jQueryUI, jQuery AJAX, ASP.NET Web Methods and a Detachable Entity Framework Data Repository

, 20 Jul 2011 CPOL
Rate this:
Please Sign up or sign in to vote.
Sortable list with jQuery persistance.

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();
 
    // apply style to the list preview for
    $("#listPreviewStyle").addClass($('#uiCssClass option:selected').text());
    var $StartIndex; 
    // Make the list that was created above Sortable
    $(".SortableList").sortable({
        start: function (event, ui) {
        // Get the start index so no database call 
        //is made if item is dropped in the same order
        $StartIndex = ui.item.index() + 1;
    },
    stop: function (event, ui) {
        // At the end of the drag, if item is in different ordinal position, 
        // update the database using the moveListItem function
        idListItem = ui.item[0].id;
        newListIndex = ui.item.index() + 1;
    if ($StartIndex != newListIndex) {
        moveListItem(idListItem, newListIndex);
        } 
            } 
    });
 
    // Add Jquery UI Classses for the sortable list - This also creates 
    // a box with a red dotted line to show
    // visually where the item will be placed on drop - see next block 
    //of code for the Styles used
    $(".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();

        // move items above current Item position down
        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();

        // move items above new position up
        items = DataContext.SortableListItems
            .Where(i => (i.SortableListId == li.SortableListId) && 
                (i.ItemOrder >= newPosition) &&
                (i.SortableListItemId != sortableListItemId));    
    
        foreach (var item in items)
            item.ItemOrder--;

        DataContext.SaveChanges();

        // move items above new position up
        items = DataContext.SortableListItems
            .Where(i => (i.SortableListId == li.SortableListId) && 
                (i.ItemOrder >= newPosition) &&
                (i.SortableListItemId != sortableListItemId));
        foreach (var item in items)
            item.ItemOrder++;

        // set the position of the item being dragged
        li.ItemOrder = newPosition;
        DataContext.SaveChanges();
        
        foreach (var item in items)
            item.ItemOrder++;

        // set the position of the item being dragged
        li.ItemOrder = newPosition;
        DataContext.SaveChanges();
    }
}

History

  • 20th July, 2011: Initial post

License

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

Share

About the Author

Wes Grant
Software Developer
United States United States
I am a general technology enthusiast and like to work on my Voodoo Content Management System and other personal programming projects in my free time.

Comments and Discussions

 
GeneralMy vote of 5 [modified] PinmemberThe_asassyn27-Jan-13 23:10 
QuestionFirstOrDefault PinmemberRichard Deeming27-Jul-11 6:02 
AnswerRe: FirstOrDefault [modified] PinmemberWes Grant27-Jul-11 7:05 
GeneralRe: FirstOrDefault PinmemberRichard Deeming27-Jul-11 8:57 
GeneralRe: FirstOrDefault PinmemberWes Grant27-Jul-11 10:05 
QuestionI love your article! PinmemberOslec26-Jul-11 14:55 
Thanks!
GeneralMy vote of 5 Pinmemberthami3620-Jul-11 4:45 
GeneralRe: My vote of 5 PinmemberWes Grant20-Jul-11 7:06 

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

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

| Advertise | Privacy | Mobile
Web03 | 2.8.141015.1 | Last Updated 20 Jul 2011
Article Copyright 2011 by Wes Grant
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid