简体   繁体   中英

C# moving an item up/down

I got a list that contains items. They all got a 'Sort' column. The sort column is of type int, and it's unique.

Scenario:

sort 1; sort 2; sort 3;

If the user moves an item up (for example sort 3) in the list (for example to position 1, which would give the sort value 1), the items that are under the one that just got moved up, have to be shifted down in the list, and the sort number should be applied accordingly. In this case all shifted items sort - 1.

So the end state of the scenario looks like this:

sort 1 was sort 3; sort 3 was sort 2; sort 3 is now sort 1;

How can i do this with LINQ ? It's not just 3 items. It can be a lot more.

[Edit]

 public ActionResult Up(int id)
 {
    var item = dataContext.item.FirstOrDefault(x => x.item == id);

    return View(dataContext.items);
 }

That might not be the easiest code to understand but I've tested it and it seems to work as intended.

Let's setup some data.

var array = new [] 
{ 
    new { Sort = 1, Value = "foo1", },
    new { Sort = 2, Value = "foo2", },
    new { Sort = 3, Value = "foo3", },
    new { Sort = 4, Value = "foo4", },
};

var oldSort = 1;
var newSort = 3;

First, query is split into three parts depending on positions of old and new indexes, so we can handle each case separately.

var q = 
    oldSort > newSort ? 
        array
            .Where(x => x.Sort >= newSort && x.Sort < oldSort)
            .Select(x => new { Sort = x.Sort + 1, Value = x.Value })
            .Union(array.Where(x => x.Sort < newSort || x.Sort > oldSort))
            .Union(array.Where(x => x.Sort == oldSort)
                        .Select(x => new { Sort = newSort, Value = x.Value }))
            : 
    oldSort < newSort ?         
        array
            .Where(x => x.Sort <= newSort && x.Sort > oldSort)
            .Select(x => new { Sort = x.Sort - 1, Value = x.Value })
            .Union(array.Where(x => x.Sort > newSort || x.Sort < oldSort))
            .Union(array.Where(x => x.Sort == oldSort)
                        .Select(x => new { Sort = newSort, Value = x.Value }))
            :
    array;

Results for moving an item down ( oldSort = 1 , newSort = 3 ):

1 foo2
2 foo3 
3 foo1 
4 foo4 

Results for moving an item up ( oldSort = 4 , newSort = 2 ):

1 foo1 
2 foo4 
3 foo2 
4 foo3 

UPDATE : The query works by splitting a sequence into three parts

  • Item with the old index becomes an item with the new index;
  • Items between the old and new indexes are shifted either up or down;
  • The rest keeps their indexes.

The result is the union of the parts.

UPDATE 2 : The query works for any number of items and the absence of loops is intentional.

UPDATE 3 : Here's one way to make the query work with LINQ-to-Entities.

using (var context = new TestDBEntities())
{
    var array = context.TestTables;
    var q =
        oldSort > newSort ?
            array
                .Where(x => x.Sort >= newSort && x.Sort < oldSort)
                .Select(x => new { Sort = x.Sort + 1, Value = x.Value })
                .Union(array.Where(x => x.Sort < newSort || x.Sort > oldSort)
                            .Select(x => new { Sort = x.Sort, Value = x.Value }))
                .Union(array.Where(x => x.Sort == oldSort)
                            .Select(x => new { Sort = newSort, Value = x.Value }))
                :
        oldSort < newSort ?
            array
                .Where(x => x.Sort <= newSort && x.Sort > oldSort)
                .Select(x => new { Sort = x.Sort - 1, Value = x.Value })
                .Union(array.Where(x => x.Sort > newSort || x.Sort < oldSort)
                            .Select(x => new { Sort = x.Sort, Value = x.Value }))
                .Union(array.Where(x => x.Sort == oldSort)
                            .Select(x => new { Sort = newSort, Value = x.Value }))
                :
        array.Select(x => new { Sort = x.Sort, Value = x.Value });
}

The difference is that the types are now explicitly compatible.

I know that you asked for a LINQ solution, but LINQ seems complicated to use in this situation, especially if you want to adjust the Sort column as well. I suggest a plain old approach using for loops and indexes. It performs the sort operation in-place and does not created a new list.

In order to make it reusable I create it as extension method for the IList interface, which makes it compatible to arrays too.

In order to make it generic, you need some way to access the Sort column. Exposing this column through an interface would restrict the solution to classes implementing this interface. Therefore I opted for accessors that you have to pass as delegates. They also work if the Sort column has another name like Order for instance.

public static class ListExtensions
{
    public static void MoveItem<T>(this IList<T> list, int fromIndex, int toIndex,
                                   Func<T, int> getSortKey, Action<T, int> setSortKey)
    {
        T temp = list[fromIndex];
        int lastSortKey = getSortKey(temp);
        setSortKey(temp, getSortKey(list[toIndex]));
        if (fromIndex > toIndex) { // Move towards beginning of list (upwards).
            for (int i = fromIndex; i > toIndex; i--) {
                list[i] = list[i - 1];
                int nextSortKey = getSortKey(list[i]);
                setSortKey(list[i], lastSortKey);
                lastSortKey = nextSortKey;
            }
        } else if (fromIndex < toIndex) { // Move towards end of list (downwards).
            for (int i = fromIndex; i < toIndex; i++) {
                list[i] = list[i + 1];
                int nextSortKey = getSortKey(list[i]);
                setSortKey(list[i], lastSortKey);
                lastSortKey = nextSortKey;
            }
        }
        list[toIndex] = temp;
    }
}

You can use the method like this

list.MoveItem(3, 1, x => x.Sort, (x, i) => x.Sort = i);

Note that you have to pass the list indexes and not the sort values.


Here are the classes I used for the tests. Just set a breakpoint at the end of the two test methods in order to inspect the result in the locals window. Start the test in the Class View by right clicking on the Test class and choosing "Invoke Static Method".

public class SomeItem
{
    public int Sort { get; set; }
    public string Value { get; set; }

    public override string ToString()
    {
        return String.Format("Sort = {0},  Value = {1}", Sort, Value);
    }
}

public static class Test
{
    public static void MoveUp()
    {
        List<SomeItem> list = InitializeList();
        list.MoveItem(3, 1, x => x.Sort, (x, i) => x.Sort = i);
    }

    public static void MoveDown()
    {
        List<SomeItem> list = InitializeList();
        list.MoveItem(1, 3, x => x.Sort, (x, i) => x.Sort = i);
    }

    private static List<SomeItem> InitializeList()
    {
        return new List<SomeItem> {
            new SomeItem{ Sort = 1, Value = "foo1" },
            new SomeItem{ Sort = 2, Value = "foo2" },
            new SomeItem{ Sort = 3, Value = "foo3" },
            new SomeItem{ Sort = 4, Value = "foo4" },
            new SomeItem{ Sort = 5, Value = "foo5" }
        };
    }

}

UPDATE

A note on adjusting the sort key: The solution above works well if the sort keys are in-order and unique. If this is not always the case, a more robust solution would be to adjust the sort keys before storing the list back to the DB by simply setting the sort key equal to the list index. This would simplify the MoveItem method.

public static void MoveItem<T>(this IList<T> list, int fromIndex, int toIndex)
{
    T temp = list[fromIndex];
    if (fromIndex > toIndex) { // Move towards beginning of list (upwards).
        for (int i = fromIndex; i > toIndex; i--) {
            list[i] = list[i - 1];
        }
    } else if (fromIndex < toIndex) { // Move towards end of list (downwards).
        for (int i = fromIndex; i < toIndex; i++) {
            list[i] = list[i + 1];
        }
    }
    list[toIndex] = temp;
}

public static void FixSortKeys<T>(this IList<T> list, Action<T, int> setSortKey)
{
    for (int i = 0; i < list.Count; i++) {
        setSortKey(list[i], i);
    }
}

The conditional operator is useful here:

var newitems = items.Select(x =>
                   new 
                   {
                       Value = x.Value,
                       Sort = x.Sort == oldSort ? newSort :
                              x.Sort < oldSort && x.Sort >= newSort ? x.Sort + 1 :
                              x.Sort > oldSort && x.Sort < newSort ? x.Sort - 1 :
                              x.Sort
                   }); 

This is using Serge's setup :

var items = new [] 
{ 
    new { Sort = 1, Value = "foo1", },
    new { Sort = 2, Value = "foo2", },
    new { Sort = 3, Value = "foo3", },
    new { Sort = 4, Value = "foo4", },
};

var oldSort = 1;
var newSort = 3;

Its performance is decent (O(n) in all scenarios), plus it's concise and readable.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM