Tuesday, March 19, 2013

Generics Can Be a Pain...

I am working on a little program for my client and I am using Sytem.Collections.Generic.List<T>s. Since most of the time I talk about SharePoint I want to make it clear what kind of list I am talking about. I'll complicate things later. ;-)

Anyway, I have two lists, in this case both lists are of type List<SPUser>. One list represents a group of SPUsers that I want to assign tasks to in a task list. The other list represents the users who already have tasks assigned to them.
My requirement is that I can not duplicate users. So, once a user has a task assigned to them, I don't want to assign a new task to them in the same list.

So, I have these two lists of the same type. I need to exclude the users from one list who exist in the other list. Simple!!, someone calls, just use List<T>.Exclude! No muss no fuss!! BZZZZZZZZZZZZZZZ Wrong answer. List<T>.Exclude will only exclude objects that are EXACTLY alike. That means that they have to come from the same instance. Essentially, if the two objects are not in the exact same memory space, Exclude will fail. This is not very intuitive, because we with the meat memories say, well they are the exact same object so it should work! The computer says, nope, the first one is from instance A, occupying memory block A, and the second one is from instance B, occupying memory block B. B!=A therefore NOT THE SAME. It really really really sucks.

Ok, we accept that life is hard and we have to do some thinking in order to get what we want to happen. So how can we do it? We could use foreach loops to iterate through the lists.

List<SPUser> thridList = new List<SPUser>();
foreach (SPUser addUser in addUserList) {
  foreach (SPUser listUser in taskListUsers) {
     if (addUser.LoginName != listUser.LoginName) {
         thridList.Add(addUser);
     }
  }
}
That is a lot of looping. Is there a better way to do this? Fortunately, there is. We can run the List<T>.RemoveAll method with a little bit of LINQ logic to essentially create the dual loop thing above.

addUserList.RemoveAll(u => taskListUsers.Any(tu => u.LoginName == tu.LoginName));

Encapsulated in this one line is the entire mess above. To read this we need to know a little about how LINQ works. If it looks confusing, you really need to learn about Lambda Expressions and Anonymous Functions.
For our purposes here, the "=>" means "WHERE", just like in a SQL query. So we are saying that we wan to RemoveAll objects in our list WHERE (=>), the following expression evaluates to TRUE. That's not really what is happening, but for this discussion it works.
From here we want to make sure we go through the entire second list. So we use the List<T>.Any() method. This method works just like a foreach loop, only the method knows that you want to go over all of the individual objects in the list.
The tricky part with the List<T>.Any() method is that it returns a bool. Therefore, what calls this method must be prepared for that. Our code is looking for a "TRUE" evaluation, so we are good to go.

Looking in to the expression inside the List<T>.Any() method, we will return true for any object that has the same SPUser.LoginName as the addUserList object item LoginName. Since it returns true, that particular item will be removed. The RemoveAll() method is already set up for negative logic, UNLIKE our looping example above. Be careful about that... When I psudocoded out the loop solution to figure out how to get the data I wanted, I did get caught by removing all of the items that I didn't want removed.

What we are left with at the end is the addUserList without any of the items who's LoginNames are the same as any of the LoginNames of the items in the taskListUsers.

Phew! A pain to be sure, but when you are using List<T> with complex types, you have to be very sure on how to manipulate your lists to get the correct data out.

No comments:

Post a Comment