2013-07-31 7 views
18

Ich versuche derzeit, eine gute Möglichkeit zu finden, meine Elemente mit LINQ und C# zu sortieren, aber ich versäume es irgendwie.LINQ sortiere eine flache Liste basierend auf Kinder

Für das Problem lassen annimmt, dass Sie die folgende Tabelle

---TempTable 
ID (int) 
ParentID (int) 
Name (varchar) 
SortOrder (int) 

Die ID und ParentID ist miteinander verwandt und geben Sie mir eine Selbst hierarchische Datenstruktur haben. Die Wurzelelemente haben eine Null im ID-Feld. Der SortOrder ist nur ein Teil der gesamten Tabelle und basiert auf der ParentID, so dass die Elemente, die dieselbe ParentID teilen, 1, 2, 3 enthalten.

Nehmen wir an, weiter die folgenden Daten:

ID = 1 
ParentID = null 
Name = Test 1 
SortOrder = 1 

ID = 2 
ParentID = 1 
Name = Test 2 
SortOrder = 1 

ID = 3 
ParentID = 1 
Name = Test 3 
SortOrder = 2 

ID = 4 
ParentID = 2 
Name = Test 4 
SortOrder = 1 

flache Liste Mein Wunsch sollte folgende Reihenfolge haben:

Test 1 //root element with sort order 1 = very top 
Test 2 //child element of root with sort order 1 
Test 4 //child element of test 2 with sort order 1 
Test 3 //child element of root with sort order 2 

Auch ich mag selbst das Objekt erhalten, ohne nur einen Teil der Informationen zu erhalten warf die Verwendung von ausgewählten neuen ...

Dies ist einer meiner gescheiterten Versuche:

from x in EntityModel.TempTables //DbSet<TempTable> by EntityFramework - which already holds all elements 
    orderby x.SortOrder 
    from y in x.TempTableChildren //Navigation Property by EntityFramework 
    orderby y.SortOrder 
    select y 

Vielen Dank im Voraus für Ihre Hilfe.

Edit:

Der Auftrag mit dem ParentID vielleicht hilfreich, mit dem angegebenen Testdata, da die ID, sind ParentIDs in perfekter Ordnung, aber dies ist nicht der Fall in einem echten Live-Anwendung seit seiner Daten getrieben, jemand könnte löschen eines Eintrags eine neue erstellen und sie in einer bestimmten Reihenfolge unter einem Elternteil platzieren und Sie müssten so etwas wie:

ID = 193475037 
ParentID = 2 
Name = Test 192375937 
SortOrder = 25 

nun in der Anwendung wäre es möglich, diese zu bewegen und die ParentID und SortOrder würde sich ändern zufällig zu etwas wie:

ID = 193475037 
ParentID = 456798424 
Name = Test 192375937 
SortOrder = 4 

furhter Um das Problem hier zu erklären, ist ein Code - wie ich es ohne 1 beautifull Linq Abfrage tun würde, aber mit 2 und einige yield return:

public class LinqTestDemo 
{ 
    Random rand = new Random(); 
    List<TempTable> list = new List<TempTable>(); 

    public List<TempTable> GetFlatData() 
    { 
     list = GetTestData(); 

     var rootElement = (from x in list 
          where x.ParentID == null 
          orderby x.SortOrder 
          select x).ToList(); 

     var flatList = OrderChilds(rootElement).ToList(); 

     foreach (var tempTable in flatList) 
     { 
      Console.WriteLine(string.Format("ID = {0} - ParentID = {1} - Name = {2} - SortOrder = {3}", tempTable.ID, tempTable.ParentID, tempTable.Name, tempTable.SortOrder)); 
     } 

     return flatList; 
    } 

    private IEnumerable<TempTable> OrderChilds(List<TempTable> enumerable) 
    { 
     foreach (var tempTable in enumerable) 
     { 
      yield return tempTable; 

      TempTable table = tempTable; 
      var childs = OrderChilds((from x in list 
             where x.ParentID == table.ID 
             orderby x.SortOrder 
             select x).ToList()); 

      foreach (var child in childs) 
      { 
       yield return child; 
      } 
     } 
    } 

    public List<TempTable> GetTestData() 
    { 
     var returnValue = new List<TempTable>(); 
     for (int i = 0; i < 50; i++) 
     { 
      var tempTable = new TempTable(); 
      tempTable.ID = i; 
      if (i == 0) 
       tempTable.ParentID = null; 
      else 
       tempTable.ParentID = rand.Next(0, i); 

      var maxSortOrder = (from x in returnValue 
           where x.ParentID == tempTable.ParentID 
           select (int?)x.SortOrder).Max(); 

      if (maxSortOrder.HasValue) 
       tempTable.SortOrder = maxSortOrder.Value + 1; 
      else 
       tempTable.SortOrder = 1; 

      tempTable.Name = string.Format("Test {0:00}", i); 
      returnValue.Add(tempTable); 
     } 

     return returnValue; 
    } 

    public class TempTable 
    { 
     public int ID { get; set; } 
     public int? ParentID { get; set; } 
     public string Name { get; set; } 
     public int SortOrder { get; set; } 
    } 
} 

@ Breiten Erste vs Depth-First Traversal: Nach einigem Lesen würde ich sagen, dass mein gewünschtes Ergebnis Tiefentiefe Traversal wäre, wo die Elemente in der gleichen Tiefentiefe von der Eigenschaft SortOrder geordnet werden sollen.

+4

Ihre Tisch-Struktur definiert eine Baumstruktur - und damit gibt es zwei Möglichkeiten, um „durchqueren“, um den Baum, um eine flache Struktur zu erzeugen, . erste Tiefe: http://www.cs.bu.edu/teaching/c/tree/breadth-first/ Breite Erstens: http://www.brpreiss.com/books/opus4/html/ page551.html Aus Ihrem Beispiel ist nicht klar, auf welchen Traversal-Typ Sie sich beziehen. –

+0

Nach einigem Lesen würde ich sagen, dass mein gewünschtes Ergebnis Tiefentiefe Traversal wäre, wo die Elemente in der gleichen Tiefentiefe von der Eigenschaft SortOrder geordnet werden sollen. –

+0

Wie viele Ebenen der Tiefe gibt es? Wenn Sie unbegrenzte Tiefe haben können, ist dies nicht in einer einzigen Abfrage möglich. Auch die Funktionsweise des Entity-Frameworks schlägt bei rekursiven Abfragen fehl. Die einzige Lösung ist das Traversieren von Bäumen. –

Antwort

12
public lEnumerable<TempTable> GetList(int? parentID = null){ 

    foreach (var item in Context.TempTables 
     .Where(x => x.ParentID == parentID) 
     .OrderBy(x=> x.SortOrder) 
     .ToList() { 

     yield return item; 

     foreach(var child in GetList(item.ID)) 
     { 
      yield return child; 
     } 

    } 
    } 


    var sortedList = GetList(); 

Es ist ähnlich wie Ihre Methode, aber es ist kleiner & rekursiv. Und funktioniert für viele Tiefenstufen. Ich bevorzuge den Aufruf von ToList, weil das Resultset vor dem Abfragen der nächsten Abfrage geschlossen wird.

Es gibt keine Möglichkeit, dies in einer einzelnen Abfrage ab sofort zu tun.

mit einzelnem Query als Gewünscht

Entity Framework automatisch alle Kinder füllen.

public IEnumerable<TempTable> PrepareList(IEnumerable<TempTable> list){ 
    list = list.OrderBy(x=> x.SortOrder); 
    foreach(var item in list){ 
     yield return item; 
     foreach(var child in PrepareList(item.ChildTempTables)){ 
      yield return child; 
     } 
    } 
} 

// since EF will automatically fill each children on fetch 
// all we need is just a top level nodes 
// which we will pass to PrepareList method 
var list = Context.TempTables.ToList().Where(x=> x.ParentID == null); 
var sortedList = PrepareList(list).ToList(); 

// it is good to create list at the end if you are going to 
// iterate it many times and logic will not change. 
+0

Könnten Sie bitte Ihren Code ändern, so dass die anfängliche Liste (in Ihrem Fall Context.TempTables) ein bereits abgefragter IEnumerable sein könnte, der von der Anforderung übergeben und sortiert wird, wie Sie die anderen 2 "neuen" Antworten (von Thomas B. und Roman Pekar) haben beide eine Lösung für dieses Szenario. Wie würdest du das machen? Danke für deine Zeit. –

+0

@RandRandom Ich habe die Antwort hinzugefügt, die den gleichen Job mit allem vorinstallierten tut. –

2

Try this:

public class Item 
{ 
    public int ID { get; set; } 
    public int? ParentID { get; set; } 
    public string Name { get; set; } 
    public int SortOrder { get; set; } 
} 

public void DoWork() 
    { 
     Item[] data = new Item[] { 
      new Item() { ID = 2, ParentID = 1, Name = "Test 2", SortOrder = 1}, 
      new Item() { ID = 3, ParentID = 1, Name = "Test 3", SortOrder = 2}, 
      new Item() { ID = 4, ParentID = 2, Name = "Test 4", SortOrder = 1}, 
      new Item() { ID = 1, ParentID = null, Name = "Test 1", SortOrder = 1}, 
     }; 

     var result = from x in data 
        orderby x.SortOrder, x.ParentID 
        select x; 

     foreach (var row in result.ToArray()) 
     { 
      Console.WriteLine(row.Name); 
     } 
    } 

Ich denke, es ist alles über die richtige Reihenfolge

+0

Bitte werfen Sie einen Blick auf meine Bearbeitung, nicht wissen, ob Sie eine Benachrichtigung über die Bearbeitung ohne diesen Kommentar erhalten. –

2

Hier ist eine einfache Lösung:

public class TempTable 
{ 
    public int ID {get;set;} 
    public int? ParentID {get;set;} 
    public String Name {get;set;} 
    public int SortOrder {get;set;} 
} 

public List<TempTable> GetTempData() 
{ 
    var temp = new List<TempTable>(); 
    temp.Add(new TempTable { ID = 1, ParentID = null, Name = "Test 1", SortOrder = 1 }); 
    temp.Add(new TempTable { ID = 2, ParentID = 1, Name = "Test 2", SortOrder = 1 }); 
    temp.Add(new TempTable { ID = 3, ParentID = 1, Name = "Test 3", SortOrder = 3 }); 
    temp.Add(new TempTable { ID = 4, ParentID = 2, Name = "Test 4", SortOrder = 1 }); 
    temp.Add(new TempTable { ID = 5, ParentID = 1, Name = "Test 5", SortOrder = 2 }); 
    return temp; 
} 

Verbrauch:

var data = GetTempData(); 
var result = data.OrderBy(d => d.SortOrder).ThenBy(d => d.ParentID); 
//Do something with result 
+0

Bitte sehen Sie sich meine Bearbeitung an, Sie wissen nicht, ob Sie eine Benachrichtigung über die Bearbeitung ohne diesen Kommentar erhalten. –

4

Hier ist eine nicht-rekursive Version. Es wird nicht immer wieder über die ursprüngliche Liste iterieren. Stattdessen verwaltet es ein Wörterbuch für die Eltern-Kind-Beziehung und speichert die aktuelle Position der laufenden Vororderbaum-Traversierung in Aufzählern.

public static IEnumerable<TempTable> PreorderForest(IEnumerable<TempTable> list) 
{ 
    var nodesByParent = list.GroupBy(x => x.ParentID.GetValueOrDefault(-1)) 
     .ToDictionary(xs => xs.Key, 
         xs => xs.OrderBy(x => x.SortOrder).GetEnumerator()); 

    var stack = new Stack<IEnumerator<TempTable>>(); 
    stack.Push(nodesByParent[-1]); 

    while (stack.Count > 0) 
    { 
     var nodes = stack.Peek(); 
     if (nodes.MoveNext()) 
     { 
      yield return nodes.Current; 
      IEnumerator<TempTable> children; 
      if (nodesByParent.TryGetValue(nodes.Current.ID, out children)) 
       stack.Push(children); 
     } 
     else 
      stack.Pop(); 
    } 
} 
+0

Könnten Sie vielleicht erklären, warum Sie eine nicht-rekursive Version erstellt haben?Führt der Stapel nicht zu einem unnötigen Overhead? –

+1

Die Standardgröße (functionn Call) für .NET-Anwendungen ist 4MB @ 64-Bit und 1MB @ 32-Bit. Die Stack-Datensammlung, die hier verwendet wird, befindet sich auf dem Heapspeicher und ist durch die Größenbeschränkung für .NET-Objektspeichergrößen von 2 GB begrenzt. Für sehr sehr tiefe Hierarchien würde eine rekursive Version eine Ausnahme weit vor der nicht-rekursiven Version auslösen. Und zusätzlich: Eine rekursive Implementierung ist normalerweise aufgrund des Calling-Overheads langsamer. (Nichtsdestoweniger: Nicht-Rekursion ist komplexer und nicht lesbar) –

3

Eigentlich weiß ich nicht, ob es durch elegante LINQ-Abfrage gemacht werden kann. Hier ist rekursive Version von DFS, es Lookup-Suche zu beschleunigen baut von ParentID

public static IEnumerable<TempTable> SortedList(IEnumerable<TempTable> list = null, int? ParentID = null, ILookup<int?, TempTable> lookup = null) 
{ 
    if (lookup == null) 
     lookup = list.ToLookup(x => x.ParentID, x => x); 

    foreach (var p in lookup[ParentID].OrderBy(x => x.SortOrder)) 
    { 
     yield return p; 
     foreach (var c in SortedList(lookup: lookup, ParentID: p.ID)) 
      yield return c; 
    } 
} 
+0

Nicht sicher, ob eine Suche in einer Entity Framework-Umgebung erforderlich ist, ist es nicht so, dass EF bereits eine integrierte Schlüsselsuche nach den primären/Fremdschlüsselwerten bietet? Wenn dies nicht der Fall ist, in welchen Szenarien ist es besser, eine Suche nach einer "normalen" LINQ-Abfrage durchzuführen, oder ist es eher eine allgemeine Sache, wenn Sie ständig einen INT-Wert abfragen, verwenden Sie Lookup? –

+0

Nun, es ist allgemeinere Lösung mit Liste, nicht Tabelle, kann nicht über eingebaute Schlüssel im Entitätsrahmen sagen. Nachschlagen hilft also, nicht für jede ID über die gesamte Liste zu iterieren. –

Verwandte Themen