使用LINQ将新的BindingList调和为主BindingList

时间:2009-10-27 20:00:37

标签: c# linq merge list

我有一个看似简单的问题,我希望协调两个列表,以便通过包含更新元素的“新”列表更新“旧”主列表。元素由关键属性表示。这些是我的要求:

  • 只有在任何属性发生变化时,任一列表中具有相同键的所有元素都会导致该元素从“旧”列表中的原始元素中分配给该元素。
  • “新”列表中包含不在“旧”列表中的键的任何元素都将添加到“旧”列表中。
  • “旧”列表中包含不在“新”列表中的键的任何元素都将从“旧”列表中删除。

我在这里找到了一个同等的问题 - Best algorithm for synchronizing two IList in C# 2.0 - 但它没有得到真正的回答。因此,我提出了一种算法来迭代旧的和新的列表并按照上面的步骤执行协调。在有人问我为什么不只是用新的列表对象完全替换旧的列表对象之前,它是出于演示目的 - 这是绑定到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&lt;&gt ;, Join&lt;&gt ;, Select&lt;&gt ;,SelectMany&lt;&gt ;,Interect&lt;&gt;等。到目前为止,我遇到的问题是这些LINQ类型方法中的每一个都会产生全新的中间列表(作为返回值),实际上,我只想修改现有列表,原因很多。

如果有人可以提供帮助,我们将不胜感激。如果没有,不用担心,上述方法(原样)就足够了。

谢谢, 杰森

5 个答案:

答案 0 :(得分:4)

你的主循环是O( m * n ),其中 m n 是旧的大小和新的名单。这很糟糕。一个更好的想法可能是首先构建关键元素映射集,然后对它们进行处理。此外,避免反射将是一个好主意 - 可以使用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 - 要么为它创建一个接口,类似于ICloneable,要么在对象之间传输属性而不是创建新实例,并让所有对象实现它;或者,通过另一个lambda将其提供给Reconcile

答案 1 :(得分:1)

我不确定BindingList,但您可以使用Continuous LINQObservableCollection<T>执行此操作。 Continuous LINQ不会定期协调列表,而是创建一个只读列表,该列表会根据您查询的列表中的更改通知进行更新,如果您的对象实现INotifyPropertyChanged,则会从列表中的对象进行更新。

这将允许您每次都使用LINQ而不生成新列表。

答案 2 :(得分:0)

建议:

而不是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
  }
}

答案 3 :(得分:0)

感谢您的回复。我使用了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稍微好一点,但是你能详细说明这个吗请leppie,in特别是,如何在我的方法中使用它来提取密钥?

有没有其他方法可以在不使用反射的情况下比较我的对象的属性,因为我不希望在可能使用此扩展方法的所有对象上实现特殊的接口?我想过在我的课程中简单地覆盖Equals,但我想尝试实现比较,而不必在可能的情况下破坏我现有的课程。

感谢。

答案 4 :(得分:0)

为了让我的解决方案的最新版本基于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,但两个列表之间的顺序保证是同义的并且附加新记录(如果它们被插入,没有附加,它仍然可以工作,但性能将是受到损害)。