Collection matching of data/view models with LINQ
-
I've written code like this before, but found an interesting way to centralize it that I feel the need to share... Basically, I'm linking a collection in my data model to a collection in my view model (M-V-VM). Since the view model is going to be bound to the GUI, I don't want to repopulate it every time there's a change on the back-end... In addition, the data objects will often be replaced instead of modified (Coming down serialized from the server), so I want the view models to just grab the new ones instead of themselves being replaced... So anyway, the objective is to iterate through the view models and data objects side-by-side, and change only that which needs to change on the view side... Normally I'd do this manually, but I decided to see how a heavy-LINQ approach would look...
PositionTradeModel[] posModels = PositionsInternal.OrderBy(a => a.Data.PositionID).ToArray();
PositionTrade[] posTrades = newData.Positions.OrderBy(a => a.PositionID).ToArray();Util.Algorithms.IterateInParallel(
posModels, posTrades,
((a, b) => a.Data.PositionID.CompareTo(b.PositionID)), // Comparer
((a, b) => a.ReplaceData(b)), // Match
((a) => PositionsInternal.Remove(a)), // Data removed
((a) => PositionsInternal.Add(new PositionTradeModel(a))) // Data added
);(PositionsInternal is an ObservableCollection, so the GUI will catch the updates right away through a ReadOnlyObservableCollection wrapper) A little tricky to read, but that's the nice thing about the XML comments built into Visual Studio... Here's the signature of the actual algorithm:
/// <summary>
/// Interate through two sorted lists in parallel, performing a side-by-side match.
/// </summary>
/// <typeparam name="TA">Type of list A</typeparam>
/// <typeparam name="TB">Type of list B</typeparam>
/// <param name="colA">List A</param>
/// <param name="colB">List B</param>
/// <param name="comparer">Comparer function to test equality between a member of each list. Should return 0 for A = B, -1 for A < B, +1 for A > B</param>
/// <param name="callbackMatch">Called when a match is found, with the two matching objects</param>
/// <param name="callbackAOnly">Called when an item is found in A that does not match anything in B</param>
/// <param name="callbackBOnly">Called when an item is found in B that does not match -
I've written code like this before, but found an interesting way to centralize it that I feel the need to share... Basically, I'm linking a collection in my data model to a collection in my view model (M-V-VM). Since the view model is going to be bound to the GUI, I don't want to repopulate it every time there's a change on the back-end... In addition, the data objects will often be replaced instead of modified (Coming down serialized from the server), so I want the view models to just grab the new ones instead of themselves being replaced... So anyway, the objective is to iterate through the view models and data objects side-by-side, and change only that which needs to change on the view side... Normally I'd do this manually, but I decided to see how a heavy-LINQ approach would look...
PositionTradeModel[] posModels = PositionsInternal.OrderBy(a => a.Data.PositionID).ToArray();
PositionTrade[] posTrades = newData.Positions.OrderBy(a => a.PositionID).ToArray();Util.Algorithms.IterateInParallel(
posModels, posTrades,
((a, b) => a.Data.PositionID.CompareTo(b.PositionID)), // Comparer
((a, b) => a.ReplaceData(b)), // Match
((a) => PositionsInternal.Remove(a)), // Data removed
((a) => PositionsInternal.Add(new PositionTradeModel(a))) // Data added
);(PositionsInternal is an ObservableCollection, so the GUI will catch the updates right away through a ReadOnlyObservableCollection wrapper) A little tricky to read, but that's the nice thing about the XML comments built into Visual Studio... Here's the signature of the actual algorithm:
/// <summary>
/// Interate through two sorted lists in parallel, performing a side-by-side match.
/// </summary>
/// <typeparam name="TA">Type of list A</typeparam>
/// <typeparam name="TB">Type of list B</typeparam>
/// <param name="colA">List A</param>
/// <param name="colB">List B</param>
/// <param name="comparer">Comparer function to test equality between a member of each list. Should return 0 for A = B, -1 for A < B, +1 for A > B</param>
/// <param name="callbackMatch">Called when a match is found, with the two matching objects</param>
/// <param name="callbackAOnly">Called when an item is found in A that does not match anything in B</param>
/// <param name="callbackBOnly">Called when an item is found in B that does not matchIts too concise and too LINQy... and just right :). I like helper functions like this. But I would change some bits: 1. Why call ToArray on the sorted lists? It should also work without doing so. 2. Instead of passing a comparing function, and two already sorted lists I would pass two delegates transforming TA and TB into something IComparable. This way the function could sort internally and the caller wouldn't have to think about this detail. The function declaration could be something like this:
public static void IterateInParallel<TA, TB, TID>(IEnumerable<TA> colA, IEnumerable<TB> colB, Func<TA, TID> idFromA, Func<TB, TID> idFromB,
Action<TA, TB> callbackMatch, Action<TA> callbackAOnly, Action<TB> callbackBOnly)
where TID : IComparable
{
colA = colA.OrderBy(a => idFromA(a));
colB = colB.OrderBy(b => idFromB(b));Func<TA, TB, int> comparer = (a, b) => idFromA(a).CompareTo(idFromB(b));
//...
}Robert
-
Its too concise and too LINQy... and just right :). I like helper functions like this. But I would change some bits: 1. Why call ToArray on the sorted lists? It should also work without doing so. 2. Instead of passing a comparing function, and two already sorted lists I would pass two delegates transforming TA and TB into something IComparable. This way the function could sort internally and the caller wouldn't have to think about this detail. The function declaration could be something like this:
public static void IterateInParallel<TA, TB, TID>(IEnumerable<TA> colA, IEnumerable<TB> colB, Func<TA, TID> idFromA, Func<TB, TID> idFromB,
Action<TA, TB> callbackMatch, Action<TA> callbackAOnly, Action<TB> callbackBOnly)
where TID : IComparable
{
colA = colA.OrderBy(a => idFromA(a));
colB = colB.OrderBy(b => idFromB(b));Func<TA, TB, int> comparer = (a, b) => idFromA(a).CompareTo(idFromB(b));
//...
}Robert
Robert Rohde wrote:
1. Why call ToArray on the sorted lists? It should also work without doing so.
So I can modify the original collections while the function is iterating :)
Robert Rohde wrote:
2. Instead of passing a comparing function, and two already sorted lists I would pass two delegates transforming TA and TB into something IComparable. This way the function could sort internally and the caller wouldn't have to think about this detail.
Thought about that... It would work for this particular situation, as I'm effectively matching them on a single integer value, but remember that these are entirely different types of objects, so while the sorting logic may be simple, the logic to compare an object in one collection to an object in the other could involve more fields. Hmm... Then again, since the matching already depends on the sorting, the functions would really HAVE to correspond, wouldn't they... Will have to give this some more thought.
Proud to have finally moved to the A-Ark. Which one are you in?
Author of the Guardians Saga (Sci-Fi/Fantasy novels) -
I've written code like this before, but found an interesting way to centralize it that I feel the need to share... Basically, I'm linking a collection in my data model to a collection in my view model (M-V-VM). Since the view model is going to be bound to the GUI, I don't want to repopulate it every time there's a change on the back-end... In addition, the data objects will often be replaced instead of modified (Coming down serialized from the server), so I want the view models to just grab the new ones instead of themselves being replaced... So anyway, the objective is to iterate through the view models and data objects side-by-side, and change only that which needs to change on the view side... Normally I'd do this manually, but I decided to see how a heavy-LINQ approach would look...
PositionTradeModel[] posModels = PositionsInternal.OrderBy(a => a.Data.PositionID).ToArray();
PositionTrade[] posTrades = newData.Positions.OrderBy(a => a.PositionID).ToArray();Util.Algorithms.IterateInParallel(
posModels, posTrades,
((a, b) => a.Data.PositionID.CompareTo(b.PositionID)), // Comparer
((a, b) => a.ReplaceData(b)), // Match
((a) => PositionsInternal.Remove(a)), // Data removed
((a) => PositionsInternal.Add(new PositionTradeModel(a))) // Data added
);(PositionsInternal is an ObservableCollection, so the GUI will catch the updates right away through a ReadOnlyObservableCollection wrapper) A little tricky to read, but that's the nice thing about the XML comments built into Visual Studio... Here's the signature of the actual algorithm:
/// <summary>
/// Interate through two sorted lists in parallel, performing a side-by-side match.
/// </summary>
/// <typeparam name="TA">Type of list A</typeparam>
/// <typeparam name="TB">Type of list B</typeparam>
/// <param name="colA">List A</param>
/// <param name="colB">List B</param>
/// <param name="comparer">Comparer function to test equality between a member of each list. Should return 0 for A = B, -1 for A < B, +1 for A > B</param>
/// <param name="callbackMatch">Called when a match is found, with the two matching objects</param>
/// <param name="callbackAOnly">Called when an item is found in A that does not match anything in B</param>
/// <param name="callbackBOnly">Called when an item is found in B that does not matchIan Shlasko wrote:
So what do you think? Too concise? Too LINQy? Or just right
absolutely right! i also prefer C# coding like this...it makes more like a functional language while keeping the good ol' imperative stuff. my colleagues are slowly going to use these neat feautures :>