【问题标题】:Reconciling a new BindingList into a master BindingList using LINQ使用 LINQ 将新的 BindingList 协调到主 BindingList
【发布时间】:2010-12-10 15:19:00
【问题描述】:

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

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

我在这里发现了一个类似的问题 - Best algorithm for synchronizing two IList in C# 2.0 - 但它并没有真正得到正确的回答。所以,我想出了一个算法来遍历旧列表和新列表,并按照上述执行协调。在有人问我为什么不只是用新列表对象完全替换旧列表对象之前,它是出于演示目的 - 这是一个绑定到 GUI 上的网格的 BindingList,我需要防止刷新工件,例如闪烁,滚动条移动等。所以列表对象必须保持不变,只有更新的元素发生了变化。

另外需要注意的是,“新”列表中的对象,即使键相同且所有属性都相同,与“旧”列表中的等效对象是完全不同的实例,因此复制参考不是一种选择。

以下是我目前所提出的 - 它是 BindingList 的通用扩展方法。我已经放入 cmets 来展示我正在尝试做的事情。

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 类型方法中的每一个都会产生全新的中间列表(作为返回值),实际上,出于上述所有原因,我只想修改现有列表。

如果有人能提供帮助,将不胜感激。如果没有,不用担心,上述方法(实际上)现在就足够了。

谢谢, 杰森

【问题讨论】:

标签: c# linq merge list


【解决方案1】:

您的主循环是 O(m*n),其中 mn 是旧的大小和新名单。这很糟糕。一个更好的想法可能是首先构建关键元素映射集,然后对其进行处理。此外,避免反射将是一个好主意 - 可以使用 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);
     }
 }

对于复制属性,我也会避免使用反射——要么为此创建一个接口,类似于ICloneable,但用于在对象之间传输属性而不是创建新实例,并让所有对象实现它;或者,通过另一个 lambda 将其提供给 Reconcile

【讨论】:

  • 感谢您的回复 Pavel - 我使用了您不错的解决方案的略微修改版本,并将代码粘贴到了关于此问题的另一个答案中。但是,是否有任何其他方法可以在不使用反射的情况下比较对象的属性,因为我不希望在可能使用此扩展方法的所有对象上实现特殊接口?我曾考虑过在我的课程中简单地覆盖 Equals,但我想尝试实现比较,而不必在可能的情况下破坏我现有的课程。谢谢。
  • 不,没有这样的方法。但是,如果您愿意覆盖Equals,我不明白为什么添加一个具有基本上等同于这种覆盖Equals 的单一方法的新接口会产生很大的不同。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

    建议:

    使用Expression&lt;Func&lt;T,object&gt;&gt; key,而不是string 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
    • 因为您只对房产名称感兴趣。事实上,您已经在表达式的主体中准备好了 PropertyInfo。现在他可以添加他的其余代码。注意,不是回答问题,只是一个建议:)
    • 感谢您的回复 - 我使用了 Pavel 非常漂亮的解决方案的略微修改版本,并将代码粘贴到了关于此问题的另一个答案中。但是,我不确定如何或为什么要使用 Expression - 这似乎比使用 Func 稍微好一点,但是请你详细说明一下,请 leppie,特别是如何使用这在我的方法中提取密钥?谢谢
    【解决方案4】:

    感谢您的回复。我使用了 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,特别是如何在我的方法中使用它来提取密钥?

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

    谢谢。

    【讨论】:

    • 抱歉,刚刚熟悉了stackoverflow系统。完成:)
    【解决方案5】:

    只是为了根据 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 在没有可用的关键字段时使用,但保证两个列表之间的顺序是同义的并追加新记录(如果插入而不追加,它仍然可以工作,但会影响性能) .

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多