提问人:Neo 提问时间:10/28/2009 最后编辑:CommunityNeo 更新时间:5/25/2010 访问量:2657
使用 LINQ 将新的 BindingList 与主 BindingList 协调
Reconciling a new BindingList into a master BindingList using LINQ
问:
我有一个看似简单的问题,我希望协调两个列表,以便将“旧”主列表更新为包含更新元素的“新”列表。元素由键属性表示。这些是我的要求:
- 任一列表中具有相同键的所有元素都会导致该元素从“新”列表分配给“旧”列表中的原始元素,前提是任何属性已更改。
- “新”列表中任何键不在“旧”列表中的元素都将添加到“旧”列表中。
- “旧”列表中任何键不在“新”列表中的元素都将从“旧”列表中删除。
我在这里发现了一个等效的问题 - 在 C# 2.0 中同步两个 IList 的最佳算法 - 但它还没有真正得到正确回答。因此,我想出了一种算法来遍历新旧列表,并按照上述方式进行对账。在有人问我为什么不只是用新的列表对象替换旧的列表对象之前,这是出于演示目的 - 这是一个绑定到 GUI 上的网格的 BindingList,我需要防止刷新伪影,例如闪烁、滚动条移动等。因此,列表对象必须保持不变,只是更改了其更新的元素。
另一件需要注意的事情是,“新”列表中的对象,即使键相同且所有属性都相同,也是与“旧”列表中的等效对象完全不同的实例,因此复制引用不是一种选择。
以下是我到目前为止想出的 - 它是 BindingList 的通用扩展方法。我已经发表评论来展示我正在尝试做的事情。
public static class BindingListExtension
{
public static void Reconcile<T>(this BindingList<T> left,
BindingList<T> right,
string key)
{
PropertyInfo piKey = typeof(T).GetProperty(key);
// Go through each item in the new list in order to find all updated and new elements
foreach (T newObj in right)
{
// First, find an object in the new list that shares its key with an object in the old list
T oldObj = left.First(call => piKey.GetValue(call, null).Equals(piKey.GetValue(newObj, null)));
if (oldObj != null)
{
// An object in each list was found with the same key, so now check to see if any properties have changed and
// if any have, then assign the object from the new list over the top of the equivalent element in the old list
foreach (PropertyInfo pi in typeof(T).GetProperties())
{
if (!pi.GetValue(oldObj, null).Equals(pi.GetValue(newObj, null)))
{
left[left.IndexOf(oldObj)] = newObj;
break;
}
}
}
else
{
// The object in the new list is brand new (has a new key), so add it to the old list
left.Add(newObj);
}
}
// Now, go through each item in the old list to find all elements with keys no longer in the new list
foreach (T oldObj in left)
{
// Look for an element in the new list with a key matching an element in the old list
if (right.First(call => piKey.GetValue(call, null).Equals(piKey.GetValue(oldObj, null))) == null)
{
// A matching element cannot be found in the new list, so remove the item from the old list
left.Remove(oldObj);
}
}
}
}
可以这样称呼:
_oldBindingList.Reconcile(newBindingList, "MyKey")
但是,我正在寻找一种使用 LINQ 类型方法(如 GroupJoin<>、Join<>、Select<>、SelectMany<>、Intersect<> 等执行相同操作的方法。到目前为止,我遇到的问题是,这些 LINQ 类型方法中的每一个都会产生全新的中间列表(作为返回值),实际上,出于上述所有原因,我只想修改现有列表。
如果有人能帮忙,将不胜感激。如果没有,不用担心,上述方法(可以说)现在就足够了。
谢谢 杰森
答:
建议:
而不是 ,请使用 。string key
Expression<Func<T,object>> key
帮助您入门的示例:
class Bar
{
string Baz { get; set; }
static void Main()
{
Foo<Bar>(x => x.Baz);
}
static void Foo<T>(Expression<Func<T, object>> key)
{
// what do we have here?
// set a breakpoint here
// look at key
}
}
评论
Expression
Func
Expression
Func
我不确定,但您可以使用 Continuous LINQ against an 来执行此操作。Continuous LINQ 不会定期协调列表,而是创建一个只读列表,该列表会根据查询列表中的更改通知以及列表中的对象(如果对象实现)的更改通知进行更新。BindingList
ObservableCollection<T>
INotifyPropertyChanged
这将允许您使用 LINQ,而无需每次都生成新列表。
您的主循环是 O(m*n),其中 m 和 n 是新旧列表的大小。这是非常糟糕的。一个更好的主意可能是先构建一组关键元素映射,然后再处理它们。此外,避免 Reflection 也是一个好主意 - 可以将 lambda 用作密钥选择器。所以:
public static void Reconcile<T, TKey>(
this BindingList<T> left,
BindingList<T> right,
Func<T, TKey> keySelector)
{
var leftDict = left.ToDictionary(l => keySelector(l));
foreach (var r in right)
{
var key = keySelector(r);
T l;
if (leftDict.TryGetValue(key, out l))
{
// copy properties from r to l
...
leftDict.RemoveKey(key);
}
else
{
left.Add(r);
}
}
foreach (var key in leftDict.Keys)
{
left.RemoveKey(key);
}
}
对于复制属性,我也会避免 Reflection - 要么为此创建一个接口,类似于 ,但用于在对象之间传输属性而不是创建新实例,并让所有对象实现它;或者,通过另一个 lambda 将其提供给。ICloneable
Reconcile
评论
Equals
Equals
感谢您的回复。我使用了 Pavel 非常漂亮的解决方案,并稍微修改了它以不使用 var 对象(并且不确定您从哪里得到),这是我的扩展方法的更新版本:RemoveKey
public static class BindingListExtension
{
public static void Reconcile<T, TKey>(this BindingList<T> left,
BindingList<T> right,
Func<T, TKey> keySelector) where T : class
{
Dictionary<TKey, T> leftDict = left.ToDictionary(key => keySelector(key));
// Go through each item in the new list in order to find all updated and new elements
foreach (T newObj in right)
{
TKey key = keySelector(newObj);
T oldObj = null;
// First, find an object in the new list that shares its key with an object in the old list
if (leftDict.TryGetValue(key, out oldObj))
{
// An object in each list was found with the same key, so now check to see if any properties have changed and
// if any have, then assign the object from the new list over the top of the equivalent element in the old list
foreach (PropertyInfo pi in typeof(T).GetProperties())
{
if (!pi.GetValue(oldObj, null).Equals(pi.GetValue(newObj, null)))
{
left[left.IndexOf(oldObj)] = newObj;
break;
}
}
// Remove the item from the dictionary so that all that remains after the end of the current loop are objects
// that were not found (sharing a key with any object) in the new list - so these can be removed in the next loop
leftDict.Remove(key);
}
else
{
// The object in the new list is brand new (has a new key), so add it to the old list
left.Add(newObj);
}
}
// Go through all remaining objects in the dictionary and remove them from the master list as the references to them were
// not removed earlier, thus indicating they no longer exist in the new list (by key)
foreach (T removed in leftDict.Values)
{
left.Remove(removed);
}
}
}
我不确定如何使用或为什么使用 - 这似乎比使用更好,但您能否更详细地阐述一下,特别是如何在我的方法中使用它来提取密钥?Expression
Func
有没有其他方法可以在不使用反射的情况下比较我的对象的属性,因为我不希望在可能使用此扩展方法的所有对象上实现一个特殊的接口?我曾考虑过在我的课程中简单地覆盖,但我想尝试实现比较,而不必尽可能中断我现有的课程。Equals
谢谢。
评论
只是为了让这个问题更新为基于 Pavel 原始答案的解决方案的最新版本,以下是最新版本的代码,它修复了原始版本的一些问题,特别是在维护秩序、专门处理 ObservableCollection 和处理没有键字段的集合方面:
internal static class ListMergeExtension
{
public static void Reconcile<T, TKey>(this IList<T> left, IList<T> right, Func<T, TKey> keySelector) where T : class
{
Dictionary<TKey, T> leftDict = left.ToDictionary(keySelector);
int index = 0;
// Go through each item in the new list in order to find all updated and new elements
foreach (T newObj in right)
{
TKey key = keySelector(newObj);
T oldObj = null;
// First, find an object in the new list that shares its key with an object in the old list
if (leftDict.TryGetValue(key, out oldObj))
{
// An object in each list was found with the same key, so now check to see if any properties have changed and
// if any have, then assign the object from the new list over the top of the equivalent element in the old list
ReconcileObject(left, oldObj, newObj);
// Remove the item from the dictionary so that all that remains after the end of the current loop are objects
// that were not found (sharing a key with any object) in the new list - so these can be removed in the next loop
leftDict.Remove(key);
}
else
{
// The object in the new list is brand new (has a new key), so insert it in the old list at the same position
left.Insert(index, newObj);
}
index++;
}
// Go through all remaining objects in the dictionary and remove them from the master list as the references to them were
// not removed earlier, thus indicating they no longer exist in the new list (by key)
foreach (T removed in leftDict.Values)
{
left.Remove(removed);
}
}
public static void ReconcileOrdered<T>(this IList<T> left, IList<T> right) where T : class
{
// Truncate the old list to be the same size as the new list if the new list is smaller
for (int i = left.Count; i > right.Count; i--)
{
left.RemoveAt(i - 1);
}
// Go through each item in the new list in order to find all updated and new elements
foreach (T newObj in right)
{
// Assume that items in the new list with an index beyond the count of the old list are brand new items
if (left.Count > right.IndexOf(newObj))
{
T oldObj = left[right.IndexOf(newObj)];
// Check the corresponding objects (same index) in each list to see if any properties have changed and if any
// have, then assign the object from the new list over the top of the equivalent element in the old list
ReconcileObject(left, oldObj, newObj);
}
else
{
// The object in the new list is brand new (has a higher index than the previous highest), so add it to the old list
left.Add(newObj);
}
}
}
private static void ReconcileObject<T>(IList<T> left, T oldObj, T newObj) where T : class
{
if (oldObj.GetType() == newObj.GetType())
{
foreach (PropertyInfo pi in oldObj.GetType().GetProperties())
{
// Don't compare properties that have this attribute and it is set to false
var mergable = (MergablePropertyAttribute)pi.GetCustomAttributes(false).FirstOrDefault(attribute => attribute is MergablePropertyAttribute);
if ((mergable == null || mergable.AllowMerge) && !object.Equals(pi.GetValue(oldObj, null), pi.GetValue(newObj, null)))
{
if (left is ObservableCollection<T>)
{
pi.SetValue(oldObj, pi.GetValue(newObj, null), null);
}
else
{
left[left.IndexOf(oldObj)] = newObj;
// The entire record has been replaced, so no need to continue comparing properties
break;
}
}
}
}
else
{
// If the objects are different subclasses of the same base type, assign the new object over the old object
left[left.IndexOf(oldObj)] = newObj;
}
}
}
Reconcile
当存在可用于比较两个列表的唯一键字段时使用。 当没有可用的键字段时使用,但保证两个列表之间的顺序是同义词,并追加新记录(如果插入,而不是追加,它仍然有效,但性能会受到影响)。ReconcileOrdered
评论