Согласование нового BindingList в основной BindingList с использованием LINQ - PullRequest
1 голос
/ 27 октября 2009

У меня, казалось бы, простая проблема, когда я хочу согласовать два списка, чтобы «старый» основной список обновлялся «новым» списком, содержащим обновленные элементы. Элементы обозначаются ключевым свойством. Это мои требования:

  • Все элементы в любом списке, имеющие одинаковый ключ, приводят к назначению этого элемента из «нового» списка поверх исходного элемента в «старом» списке, только если какие-либо свойства были изменены.
  • Любые элементы в «новом» списке, ключи которых отсутствуют в «старом» списке, будут добавлены в «старый» список.
  • Все элементы в «старом» списке, ключи которых отсутствуют в «новом» списке, будут удалены из «старого» списка.

Я нашел здесь эквивалентную проблему - Лучший алгоритм для синхронизации двух IList в C # 2.0 - но на самом деле он не получил правильного ответа. Итак, я придумал алгоритм для перебора старого и нового списков и выполнения сверки в соответствии с вышеизложенным. Прежде чем кто-либо спросит, почему я не просто заменяю старый объект списка новым объектом списка целиком, он предназначен для целей презентации - это 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 приводит к совершенно новым промежуточным спискам (в качестве возвращаемого значения), и на самом деле, я только хочу изменить существующий список по всем вышеуказанным причинам.

Если кто-нибудь может помочь с этим, был бы очень признателен. Если нет, то не беспокойтесь, вышеописанного метода (как бы) пока хватит.

Спасибо, Jason * +1027 *

Ответы [ 5 ]

4 голосов
/ 27 октября 2009

Ваш основной цикл O ( m * n ), где m и n - размеры старого и нового списков. Это довольно плохо. Лучше было бы сначала создать наборы сопоставлений ключевых элементов, а затем поработать над ними. Также было бы неплохо избегать Reflection - вместо ключевого селектора можно использовать лямбду. Итак:

 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, но для передачи свойств между объектами, а не для создания новых экземпляров, и пусть все ваши объекты реализуют его; или поставьте его на Reconcile через другую лямбду.

1 голос
/ 27 октября 2009

Я не уверен насчет BindingList, но вы можете использовать Continuous LINQ против ObservableCollection<T>, чтобы сделать это. Вместо того, чтобы периодически сверять список, Continuous LINQ создаст список только для чтения, который обновляется в результате уведомлений об изменениях из запрашиваемого вами списка и, если ваши объекты реализуют INotifyPropertyChanged, из объектов в списке.

Это позволит вам использовать LINQ без создания нового списка каждый раз.

0 голосов
/ 24 мая 2010

Просто для того, чтобы этот Вопрос обновлялся последним воплощением моего решения на основе оригинального ответа Павла, вот последняя версия кода, которая исправляет некоторые проблемы с исходной версией, особенно в том, что касается поддержания порядка, обработки 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 используется, когда нет доступного ключевого поля, но порядок между двумя списками гарантированно является синонимом, и новые записи добавляются (если они вставлены, не добавлены, все равно будут работать, но производительность будет снижена) .

0 голосов
/ 29 октября 2009

Спасибо за ответы. Я использовал очень изящное решение Павла и немного изменил его, чтобы не использовать объекты 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 в моих классах, но я хочу попытаться добиться сравнения, не прерывая мои существующие классы, если это возможно.

Спасибо.

0 голосов
/ 27 октября 2009

Предложение:

Вместо 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
  }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...