atconway
atconway

Reputation: 21304

How do I order a collection based on a child property using LINQ or a Lambda?

I get the following string expression provided:

"ChildObject.FullName" ...where ChildObject is an instance property on the MyObject1 type.

ChildObject has a property named "FullName" and I want to sort a collection of type "MyObject1" based on this child properties "FullName" value.

I can do this all day long on properties directly on MyObject1 but I run into 2 challanges when doing it on a child instance and I can't get all the pieces working. The main 2 challanges are:

  1. MyObject1 has a few different child property types so I can't hardcode the type for ChildObject. The string could be of any type.
  2. The sort expression is a String and not a known type.

For #2 above I can use Reflection to get the type of the child property, but I just can't get it all to work. I have the following below and it compiles and runs, but does not sort any differently:

'SortExpression below is a String like: "ChildObject.FullName"
MyObject1List = MyObject1List.OrderBy(Function(x)
      Dim t As Type = x.GetType()
      Dim tp As Type = t.GetProperty(SortExpression.Split(".").ElementAt(0)).PropertyType()
      Return tp.GetProperty(Request.CompareExpression.Split(".").ElementAt(1))
      End Function).ToList()

Above the value returned from the last line in the expression (if I run the code outsode the OrderBy method, does provide me the 'FullName' information I need. So the code must be close, but it still does not work.

Any ideas on how I can accomplish this? What I am trying to prevent is hardcoding a series of 'If' blocks on the child's type to then hardcode in its type to the sort or OrderBy method.

Thanks!

Upvotes: 1

Views: 2087

Answers (2)

AlanT
AlanT

Reputation: 3663

If I understand this correctly you have an object (say, Parent) which contains an object (say, Child). Child has a field FullName and you want to sort a list of parents by the child FullName.

If so, then then OrderBy() should do it for you.

Say we have a list of parents

Parent{ Id = 1, Child = { Id = 1, FullName = "Jan"}} Parent{ Id = 2, Child = { Id = 2, FullName = "Feb"}} Parent{ Id = 3, Child = { Id = 3, FullName = "Mar"}} Parent{ Id = 4, Child = { Id = 4, FullName = "Apr"}}

sorting them using OrderBy()

Dim sorted = Items.OrderBy(Function(itm) itm.Child.FullName)

gives

Parent{ Id = 4, Child = { Id = 4, FullName = "Apr"}} Parent{ Id = 2, Child = { Id = 2, FullName = "Feb"}} Parent{ Id = 1, Child = { Id = 1, FullName = "Jan"}} Parent{ Id = 3, Child = { Id = 3, FullName = "Mar"}}

(example below)

hth,
Alan.

Edit

Just re-read the question. You have different FullName properties which are selected by name (from a query string?)

You can select the property by name using an expression (see, How can I create a dynamic Select on an IEnumerable<T> at runtime?) for the general shape.

If the selected property is IComparable (Or is it IEquatable? Not too sure which) then the OrderBy() will still work. This means that as long as the sort fields are basic types you are fine. If they are custom types (objects) you will need to do some more work...

Sorry about the first mis-answer.

More Edits

It's Friday and slow in here :?

OK, expanded the answer to access different child memebers by name. (I used Fields rather than Properties but either will work). We still need to know the type of the field but a bit more work might remove that (if needed).

Private Class Child
Public Id As Integer
Public FullName As String
End Class

Private Class Parent
Public Id As Integer
Public Child As Child
End Class

Private Items As New List(Of Parent)() From { _
New Parent() With { _
    Key .Id = 1, _
    Key .Child = New Child() With { _
        Key .Id = 1, _
        Key .FullName = "Jan" _
    } _
}, _
New Parent() With { _
    Key .Id = 2, _
    Key .Child = New Child() With { _
        Key .Id = 2, _
        Key .FullName = "Feb" _
    } _
}, _
New Parent() With { _
    Key .Id = 3, _
    Key .Child = New Child() With { _
        Key .Id = 3, _
        Key .FullName = "Mar" _
    } _
}, _
New Parent() With { _
    Key .Id = 4, _
    Key .Child = New Child() With { _
        Key .Id = 4, _
        Key .FullName = "Apr" _
    } _
} _
}

<TestMethod> _
Public Sub SortByChildName()
Dim expectedParentIds = New () {4, 2, 1, 3}
Dim sortedIds = Items.OrderBy(SelectExpression(Of Parent, String)("Child.FullName")).[Select](Function(itm) itm.Id)

Assert.IsTrue(expectedParentIds.SequenceEqual(sortedIds))
End Sub

<TestMethod> _
Public Sub SortByChildId()

Dim expectedParentIds = New () {4, 3, 2, 1}
Dim sortedIds = Items.OrderBy(SelectExpression(Of Parent, Integer)("Child.Id")).[Select](Function(itm) itm.Id)

Assert.IsTrue(expectedParentIds.SequenceEqual(sortedIds))

End Sub


Public Shared Function SelectExpression(Of TItem, TField)(fieldNames As String) As Func(Of TItem, TField)

Dim type = GetType(TItem)
Dim fields = fieldNames.Split("."C)

Dim arg As ParameterExpression = Expression.Parameter(type, "item")
Dim expr As Expression = arg

For Each field As String In fields
    Dim fieldInfo = type.GetField(field)
    expr = Expression.Field(expr, fieldInfo)
    type = fieldInfo.FieldType
Next

Return Expression.Lambda(Of Func(Of TItem, TField))(expr, arg).Compile()

End Function

Upvotes: 1

kmp
kmp

Reputation: 10865

If I understand the question correctly (disclaimer - this is the first vb.net code I have ever written, it may not be the best, syntactically - I wrote it in c# first), one way you could achieve this is by doing the following...

Let's say your MyObject1 looks like this:

Public Class MyObject1

    Private mChildObject As SortableChildObject

    Public Property ChildObject() As SortableChildObject
        Get
            ChildObject = mChildObject
        End Get
        Set(value As SortableChildObject)
            mChildObject = value
        End Set
    End Property
End Class

Notice it has a property that must be "SortableChildObject" - that class looks like this:

' Implement IComparable using reflection - just look up the property to
' sort on based on the "SortExpression" property 
Public MustInherit Class SortableChildObject
    Implements IComparable

    Protected MustOverride ReadOnly Property SortExpression() As String

    Public Function CompareTo(obj As Object) As Integer Implements System.IComparable.CompareTo

        ' Make sure the object we are comparing to is also our type
        Dim oo As SortableChildObject = TryCast(obj, SortableChildObject)
        If oo Is Nothing Then
            Throw New ArgumentException("I cannot compare these two objects")
        End If

        ' Get the value to sort on for this object
        Dim thisVal As IComparable = GetSortableValue(Me, SortExpression)
        If thisVal Is Nothing Then
            Throw New ArgumentException("Could not get the value of the sortable property for this")
        End If

        ' Get the value to sort on for the object we are comparing to
        Dim thatVal As IComparable = GetSortableValue(oo, oo.SortExpression)
        If thatVal Is Nothing Then
            Throw New ArgumentException("Could not get the value of the sortable property for that")
        End If

        ' Use the IComparable implementation of the properties we are comparing
        Return thisVal.CompareTo(thatVal)
    End Function

    Private Function GetSortableValue(obj As Object, sortExpression As String) As IComparable

        Dim prop As PropertyInfo = obj.GetType().GetProperty(sortExpression)
        If prop Is Nothing Then
            Throw New ArgumentException("Could not find the property " + sortExpression)
        End If

        Dim val As Object = prop.GetValue(obj, Nothing)

        Dim ret As IComparable = TryCast(val, IComparable)
        If ret Is Nothing Then
            Throw New ArgumentException("No way to compare the values as the comparable property does not implement IComparable")
        End If

        Return ret
    End Function
End Class

Now what you must do is make sure that all of the things that you want to sort inherit from this class, for example say we have something that has a "FullName" String property on it, it would look like this:

' This is a child object that has a string property called "FullName" which
' is what we want to sort on
Public Class FullNameChildObject
    Inherits SortableChildObject

    Private mFullName As String

    Protected Overrides ReadOnly Property SortExpression() As String
        Get
            SortExpression = "FullName"
        End Get
    End Property

    Public Property FullName() As String
        Get
            FullName = mFullName
        End Get
        Set(value As String)
            mFullName = value
        End Set
    End Property

End Class

So to use this, let's construct a little list of objects to sort like so:

Dim myObject1List As New List(Of MyObject1)

Dim i As FullNameChildObject = New FullNameChildObject
i.FullName = "B"

Dim o As New MyObject1
o.ChildObject = i
myObject1List.Add(o)

i = New FullNameChildObject
i.FullName = "A"
o = New MyObject1
o.ChildObject = i
myObject1List.Add(o)

i = New FullNameChildObject
i.FullName = "D"
o = New MyObject1
o.ChildObject = i
myObject1List.Add(o)

i = New FullNameChildObject
i.FullName = "C"
o = New MyObject1
o.ChildObject = i
myObject1List.Add(o)

Sorting it based on the ChildObject property is then very simple, all you need to do is:

Dim ret = myObject1List.OrderBy(Function(x)
                                    Return x.ChildObject
                                End Function)

Is that what you need? It is a bit problematic doing it this way - as you see there are lots of places where something could go wrong - if, for example, you mix the objects that you want to compare up (say, you have one that sorts on an integer and another by string) it will throw an exception.

Upvotes: 2

Related Questions