代码之家  ›  专栏  ›  技术社区  ›  Neo

使用LINQ将新的绑定列表协调为主绑定列表

  •  1
  • Neo  · 技术社区  · 15 年前

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

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

    我在这里找到了一个等价的问题- Best algorithm for synchronizing two IList in C# 2.0 -但它并没有得到正确的回答。所以,我想出了一个算法,可以遍历新列表和旧列表,并按照上面的内容执行对帐。在有人问我为什么不将旧的列表对象全部替换为新的列表对象之前,这是为了表示的目的——这是绑定到GUI上网格的绑定列表,我需要防止刷新工件,如闪烁、滚动条移动等,因此列表对象必须保持不变,只有更新的元素发生了更改。

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

    下面是我到目前为止提出的方法——它是一个绑定列表的通用扩展方法。我发表了一些评论来证明我想做什么。

    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、Select、Selectmany、Intersect等)执行相同操作的方法。到目前为止,我遇到的问题是,每种LINQ类型方法都会产生全新的中间列表(作为返回值),实际上,我只想修改出于上述所有原因,他现有的名单。

    如果有人能帮上忙,我将不胜感激。如果没有,不用担心,上述方法(实际上)目前就足够了。

    谢谢, 杰森

    5 回复  |  直到 15 年前
        1
  •  4
  •   Pavel Minaev    15 年前

    你的主回路是O( * n )在哪里 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);
         }
     }
    

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

        2
  •  1
  •   GraemeF    15 年前

    我不确定 BindingList 但是你可以用 Continuous LINQ 反对 ObservableCollection<T> 这样做。连续的LINQ将创建一个只读列表,而不是定期地协调列表,该列表将根据您查询的列表中的更改通知进行更新,如果您的对象实现了 INotifyPropertyChanged ,从列表中的对象。

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

        3
  •  0
  •   leppie    15 年前

    建议:

    而不是 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
      }
    }
    
        4
  •  0
  •   Neo    15 年前

    谢谢你的回复。我使用了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 在我的课程中,但是我想尝试实现比较,如果可能的话,不必破坏我现有的课程。

    谢谢。

        5
  •  0
  •   Neo    15 年前

    为了根据Pavel的原始答案将这个问题更新为我的解决方案的最新版本,下面是代码的最新版本,它修复了原始版本的一些问题,特别是在维护顺序、特别处理可观察到的集合以及处理没有关键字段的集合方面:

    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 当没有可用的关键字字段时使用,但两个列表之间的顺序保证是同义的,并且附加了新记录(如果插入了它们,而不是附加了它们,它仍然有效,但性能将受到影响)。