StartledCat Generic List<> Comparer

25-Mar-2018 - Updates to the Class

Having slept on it, I have made a few tweeks to the class so that it has a bit more functionality and meaningful method names:
 public class ListComparer<T>
    {
        private List<T> MasterList;
        private List<T> ChangedList;
        private IEqualityComparer<T> EqualityComparer;

        public ListComparer(List<T> masterList, List<T> changedList, IEqualityComparer<T> comparer)
        {
            MasterList = masterList;
            ChangedList = changedList;
            EqualityComparer = comparer;
        }

        // listA.Except(listB) will give you all of the items in listA that are not in listB
        public List<T> ResultListInserted(Expression<Func<T, bool>> predicate)
        {
            // .Except acts like DISTINCT, so have to use predicate
            return ChangedList.Where(predicate.Compile()).ToList();
        }

        /// <summary>
        /// Use this if unique new ids are allocated to the items in the list
        /// </summary>
        /// <returns></returns>
        public List<T> ResultsListInsertedDistinct()
        {
            return ChangedList.Except(MasterList, EqualityComparer).ToList();
        }

        public List<T> ResultListDeleted()
        {
            return MasterList.Except(ChangedList, EqualityComparer).ToList();
        }

        public List<T> ResultListIntersect()
        {
            return MasterList.Intersect(ChangedList, EqualityComparer).ToList();
        }

        /// 
        /// Using predicate to accept something like (c => c.IsDirty != 0)
        /// NB This is the same as ResultListInserted
        /// 
        /// 
        /// 
        public List<T> ResultListUpdated(Expression<Func<T, bool>> predicate)
        {
            return ChangedList.Where(predicate.Compile()).ToList();
        }

    }

Having revisited unbound DataGridViews, the use of single row updates as and when changes are made is not the best practice when taking changing the UI layer into consideration.
Best practice, simply put, is to present a 'Model' to the 'View', the 'View' being what the user sees, and the 'Model' being the data that is displayed.

The Model should be built in a layer under/behind the View such that if the View is removed and changed for something else, the Model will still there for use with a different UI.

In terms of a grid, there will be two sets of data - the master data and the data returned from the UI.
Then, going back to basics, anything missing is a Delete, anything new is an Insert and anything changed is an Update.

In my case, the view is a screen with a grid on it that is populated from a simple List<> of records.
This means I have two List<>s i.e. master (unchanged) and the one returned from the screen (changed?)

Hence, I needed/need a bunch of code to compare the lists and work out what needs to be done.

On-line searching provided some insight into short-cuts that can be used, but nothing generic popped up i.e. everything was specific to a single list of a particular class rather than a generic List<T>.

After a bit head scratching and tinkering with List<T> and Linq, the 'best' implementation I have so far, is as follows...>/div>

Things to Note:
  • No data changes are made - this only supplies the 3 Lists corresponding to Inserted, Deleted and the 'row' that are the same and need checking for changes;
  • The caller needs to define a compare function (IEqualityComparer);
  • In my case, new rows are added with Id = 0. The List<>.Except works like DISTINCT so will not work if more than one row is added;
  • Still need to find a 'nice' way of identifying changes rather than iterating through the lists.
  • My Test class that will be used for Master and Changed:
        public class TestClass
        {
            public int Id { get; set; }
            public string Path { get; set; }
            public string Caption { get; set; }
            public string Description { get; set; }
            public bool CoverImage { get; set; }
            public bool Visible { get; set; }
        }
    
    A comparer = required to compare different instances of the TestClass:
        public class TestComparer : IEqualityComparer<TestClass>
        {
            public bool Equals(TestClass x, TestClass y)
            {
    
                //Check whether the compared objects reference the same data.
                if (Object.ReferenceEquals(x, y)) return true;
    
                //Check whether any of the compared objects is null.
                if (x is null || y is null)
                    return false;
    
                return x.Id == y.Id;
            }
    
            // If Equals() returns true for a pair of objects 
            // then GetHashCode() must return the same value for these objects.
    
            public int GetHashCode(TestClass accommodationImageModel)
            {
                //Check whether the object is null
                if (accommodationImageModel is null) return 0;
    
                //Get hash code for the Code field.
                int hashProductCode = accommodationImageModel.Id.GetHashCode();
    
                //Calculate the hash code for the product.
                return hashProductCode;
            }
        }
    
    And this is the class that supplies the 'split' list:
        public class ListComparer<T>
        {
            private List<T> MasterList;
            private List<T> ChangedList;
            private IEqualityComparer<T> EqualityComparer;
    
            public ListComparer(List<T> masterList, List<T> changedList, IEqualityComparer<T> comparer)
            {
                MasterList = masterList;
                ChangedList = changedList;
                EqualityComparer = comparer;
            }
    
            // listA.Except(listB) will give you all of the items in listA that are not in listB
            public List<T> ResultListInserted(Expression<Func<T, bool>> predicate)
            {
                // .Except acts like DISTINCT, so have to use predicate
                return ChangedList.Where(predicate.Compile()).ToList();
            }
    
            public List<T> ResultListDeleted()
            {
                return MasterList.Except(ChangedList, EqualityComparer).ToList();
            }
    
            public List<T> ResultListCheckUpdated()
            {
                return MasterList.Intersect(ChangedList, EqualityComparer).ToList();
            }
    
        }
    
    ...and I have some test code that looks like this:
        [TestClass()]
        public class ListComparerTests
        {
            [TestMethod]
            public void Test_ListComparerTests()
            {
                // set up the masterList
                List<TestClass> masterList = new List<TestClass>();
                masterList.Add(new TestClass { Id = 1 });
                masterList.Add(new TestClass { Id = 2 });
    
                // set up the compare list
                List<TestClass> compareList = new List<TestClass>();
                compareList.Add(new TestClass { Id = 1 });
                compareList.Add(new TestClass { Id = 0 });
                compareList.Add(new TestClass { Id = 0 });
    
                // get the updated models
                // direct look up
                //List result = masterList.Where(c => compareList.Any(d => c.Id == d.Id)).ToList();
    
                // generic class comparer
                TestComparer objectComparer = new TestComparer();
    
                ACLib_Core.ListComparer<TestClass> listComparer = new ACLib_Core.ListComparer<TestClass>(masterList, compareList, objectComparer);
    
                List<TestClass> resultList_Updates = listComparer.ResultListCheckUpdated();
                List<TestClass> resultList_Inserts = listComparer.ResultListInserted(c => c.Id == 0);
                List<TestClass> resultList_Deletes = listComparer.ResultListDeleted();
    
                // List result =
                // result should hold first model with id 1
                Assert.AreEqual(1, resultList_Updates.FirstOrDefault().Id);
                Assert.AreEqual(1, resultList_Updates.Count);
    
                Assert.AreEqual(0, resultList_Inserts.FirstOrDefault().Id);
                Assert.AreEqual(2, resultList_Inserts.Count);
    
                Assert.AreEqual(2, resultList_Deletes.FirstOrDefault().Id);
                Assert.AreEqual(1, resultList_Deletes.Count);
    
            }
        }
    

    © 2018 - StartledCat