3

I originally set the datasource of the DGV to a SortableBindingList. When I run the program, I am able to click any column header and sort ascending or descending by the column.

I have implemented a Filter textbox that filters the data in the DGV using LINQ. After I filter the list with LINQ. I rebind the filtered list to the DGV, but the previously sorted column is no longer sorted.

Should the DGV keep sorting even after I rebind a new datasource?

The only workaround I have come up with for this is to store the current SortedColumn Index and current SortOrder into variables and then reset those properties when binding the new datasource.

private void PopulateGrid()
{
     var gridSource = new MySortableBindingList<Case>(_caseList);
     dataGridView_Cases.DataSource = gridSource;
     ConfigureGrid();
}

private void ApplyFilter(string fString)
{
        MySortableBindingList<Case> msbList = new MySortableBindingList<Case>(_caseList.Where(x => (x.StudentLastName.IndexOf(fString, StringComparison.OrdinalIgnoreCase) >= 0) || (x.StudentIDDisplay.ToString().IndexOf(fString, StringComparison.OrdinalIgnoreCase) >= 0)).ToList());
        dataGridView_Cases.DataSource = msbList;                       
}

Update 1: (New Code)

    private MySortableBindingList<Case> _gridSource = new MySortableBindingList<Case>();
    BindingSource _caseBindingSource = new BindingSource();
    private void PopulateGrid()
    {
        _gridSource = new MySortableBindingList<Case>(_caseList);
        _caseBindingSource.DataSource = _gridSource;
        dataGridView_Cases.DataSource = _caseBindingSource;
        ConfigureGrid();
    }

 private void ApplyFilter(string fString)
 {
     _gridSource.Clear();
     foreach (var fCase in _caseList.Where(x => (x.StudentLastName.IndexOf(fString, StringComparison.OrdinalIgnoreCase) >= 0) || (x.StudentIDDisplay.ToString().IndexOf(fString, StringComparison.OrdinalIgnoreCase) >= 0)).ToList())
       {
          _gridSource.Add(fCase);
       }
      _caseBindingSource.ResetBindings(false);
 }

Update 2: (Additional Code)

/// <summary>
/// Source: http://www.codeproject.com/Articles/31418/Implementing-a-Sortable-BindingList-Very-Very-Quic
/// </summary>
/// <typeparam name="T"></typeparam>
public class MySortableBindingList<T> : BindingList<T>
{

    // reference to the list provided at the time of instantiation
    List<T> originalList;
    ListSortDirection sortDirection;
    PropertyDescriptor sortProperty;

    // function that refereshes the contents
    // of the base classes collection of elements
    Action<MySortableBindingList<T>, List<T>>
                   populateBaseList = (a, b) => a.ResetItems(b);

    // a cache of functions that perform the sorting
    // for a given type, property, and sort direction
    static Dictionary<string, Func<List<T>, IEnumerable<T>>>
       cachedOrderByExpressions = new Dictionary<string, Func<List<T>,
                                                 IEnumerable<T>>>();
    /// <summary>
    /// Create a sortable binding list
    /// </summary>
    public MySortableBindingList()
    {
        originalList = new List<T>();
    }

            /// <summary>
    /// Create a sortable binding list
    /// </summary>
    public MySortableBindingList(IEnumerable<T> enumerable)
    {
        originalList = enumerable.ToList();
        populateBaseList(this, originalList);
    }

    /// <summary>
    /// Create a sortable binding list
    /// </summary>
    public MySortableBindingList(List<T> list)
    {
        originalList = list;
        populateBaseList(this, originalList);
    }

    /// <summary>
    /// Look for an appropriate sort method in the cache if not found .
    /// Call CreateOrderByMethod to create one. 
    /// Apply it to the original list.
    /// Notify any bound controls that the sort has been applied.
    /// </summary>
    /// <param name="prop"></param>
    /// <param name="direction"></param>
    protected override void ApplySortCore(PropertyDescriptor prop,
                            ListSortDirection direction)
    {
        /*
         Look for an appropriate sort method in the cache if not found .
         Call CreateOrderByMethod to create one. 
         Apply it to the original list.
         Notify any bound controls that the sort has been applied.
         */

        sortProperty = prop;

        var orderByMethodName = sortDirection ==
            ListSortDirection.Ascending ? "OrderBy" : "OrderByDescending";
        var cacheKey = typeof(T).GUID + prop.Name + orderByMethodName;

        if (!cachedOrderByExpressions.ContainsKey(cacheKey))
        {
            CreateOrderByMethod(prop, orderByMethodName, cacheKey);
        }

        ResetItems(cachedOrderByExpressions[cacheKey](originalList).ToList());
        ResetBindings();
        sortDirection = sortDirection == ListSortDirection.Ascending ?
                        ListSortDirection.Descending : ListSortDirection.Ascending;
    }


    private void CreateOrderByMethod(PropertyDescriptor prop,
                 string orderByMethodName, string cacheKey)
    {

        /*
         Create a generic method implementation for IEnumerable<T>.
         Cache it.
        */

        var sourceParameter = Expression.Parameter(typeof(List<T>), "source");
        var lambdaParameter = Expression.Parameter(typeof(T), "lambdaParameter");
        var accesedMember = typeof(T).GetProperty(prop.Name);
        var propertySelectorLambda =
            Expression.Lambda(Expression.MakeMemberAccess(lambdaParameter,
                              accesedMember), lambdaParameter);
        var orderByMethod = typeof(Enumerable).GetMethods()
                                      .Where(a => a.Name == orderByMethodName &&
                                                   a.GetParameters().Length == 2)
                                      .Single()
                                      .MakeGenericMethod(typeof(T), prop.PropertyType);

        var orderByExpression = Expression.Lambda<Func<List<T>, IEnumerable<T>>>(
                                    Expression.Call(orderByMethod,
                                            new Expression[] { sourceParameter, 
                                                           propertySelectorLambda }),
                                            sourceParameter);

        cachedOrderByExpressions.Add(cacheKey, orderByExpression.Compile());
    }

    /// <summary>
    /// RemoveSortCore
    /// </summary>
    protected override void RemoveSortCore()
    {
        ResetItems(originalList);
    }

    private void ResetItems(List<T> items)
    {

        base.ClearItems();

        for (int i = 0; i < items.Count; i++)
        {
            base.InsertItem(i, items[i]);
        }
    }

    /// <summary>
    /// SupportsSortingCore
    /// </summary>
    protected override bool SupportsSortingCore
    {
        get
        {
            // indeed we do
            return true;
        }
    }

    /// <summary>
    /// Ascending or descending
    /// </summary>
    protected override ListSortDirection SortDirectionCore
    {
        get
        {
            return sortDirection;
        }
    }

    /// <summary>
    /// A property
    /// </summary>
    protected override PropertyDescriptor SortPropertyCore
    {
        get
        {
            return sortProperty;
        }
    }

    /// <summary>
    /// List has changed
    /// </summary>
    /// <param name="e"></param>
    protected override void OnListChanged(ListChangedEventArgs e)
    {
        originalList = base.Items.ToList();
    }
}
Michael
  • 689
  • 8
  • 24
  • Makes sense that sort order wouldn't be retained between changing `DataSources`; consider a situation where the new `DataSource` doesn't have all the columns (read: the one that was sorted) from the old `DataSource`. I believe storing them in variables and programatically re-assigning the sort data is the only way to do it. – sab669 Oct 14 '15 at 18:34
  • Why are you reassigning the data source all the time? Assign it once, then modify the content and raise Reset event. – Ivan Stoev Oct 14 '15 at 18:37
  • @IvanStoev How can I modify the content after it bound as the data-source? What Reset event? – Michael Oct 14 '15 at 19:46
  • https://msdn.microsoft.com/en-us/library/ms132702(v=vs.110).aspx – Ivan Stoev Oct 14 '15 at 19:51
  • @IvanStoev Calling the ResetBindings() method doesn't make the datagridview refreshing the datasource. It still seems like reassigning the datasource is the only way to get the datagridview to update to the most current data – Michael Oct 14 '15 at 20:16
  • I'm using this for years and it's indeed working. But you need first to replace the content of the binding list (you know, using Clear, Add etc.) instead of doing `new MySortableBindingList` – Ivan Stoev Oct 14 '15 at 20:37
  • @IvanStoev Finally got it to function the way you are talking about by using a BindingSource in between the the DGV and the BindingList, however, even though it functions, the sorted column's sort order is still lost each time the list is changed. I have updated my original post to show the code that i am now using based on your comments. – Michael Oct 14 '15 at 21:05
  • @user3653277 No, BindingSource is not needed, the sort info and functionality is maintained/provided by the BindingList implementation. Could you please post the full code of your `MySortableBindingList` because it's the key for making the whole thing to work correctly. – Ivan Stoev Oct 15 '15 at 08:37
  • @IvanStoev I have added the code of the MySortableBindingList class to my original post – Michael Oct 15 '15 at 12:13

2 Answers2

3

DataGridView does not contain own sorting capability, but instead relies on the data source to do that, and more specifically the IBindingList implementation. Additional to sorting, IBindingList provides a ListChanged event which can be used to update any UI attached to it. Note that IBindingList is also a list, i.e. you can add/remove/update items as with a normal list, hence you don't need to create a new list and reassign it as a data source. Instead, create it once, attach the UI to it, and then at anytime just update the list content and the UI will reflect the changes automatically - one of the data binding "magics".

Now, in order to make all that happen, IBindingList must be implemented correctly. BindingList<T> already provides the most of the functionality required, but unfortunately that does not include sorting. The problems you are experiencing originate from the fact that you are using a buggy third party component (as usual when there is no standard one).

So, instead of MySortableBindindingList<T> use the following implementation:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Linq;
using System.Linq.Expressions;
namespace Tests
{
    public class MyBindingList<T> : BindingList<T>
    {
        static readonly Dictionary<string, Func<IEnumerable<T>, IEnumerable<T>>> orderByMethodCache = new Dictionary<string, Func<IEnumerable<T>, IEnumerable<T>>>();
        private static Func<IEnumerable<T>, IEnumerable<T>> GetOrderByMethod(PropertyDescriptor prop, ListSortDirection direction)
        {
            var orderByMethodName = direction == ListSortDirection.Ascending ? "OrderBy" : "OrderByDescending";
            var cacheKey = typeof(T).GUID + prop.Name + orderByMethodName;
            Func<IEnumerable<T>, IEnumerable<T>> orderByMethod;
            if (!orderByMethodCache.TryGetValue(cacheKey, out orderByMethod))
                orderByMethodCache.Add(cacheKey, orderByMethod = CreateOrderByMethod(prop, orderByMethodName));
            return orderByMethod;
        }
        private static Func<IEnumerable<T>, IEnumerable<T>> CreateOrderByMethod(PropertyDescriptor prop, string orderByMethodName)
        {
            var source = Expression.Parameter(typeof(IEnumerable<T>), "source");
            var item = Expression.Parameter(typeof(T), "item");
            var member = Expression.Property(item, prop.Name);
            var selector = Expression.Lambda(member, item);
            var orderByMethod = typeof(Enumerable).GetMethods()
                .Single(a => a.Name == orderByMethodName && a.GetParameters().Length == 2)
                .MakeGenericMethod(typeof(T), member.Type);
            var orderByExpression = Expression.Lambda<Func<IEnumerable<T>, IEnumerable<T>>>(
                Expression.Call(orderByMethod, new Expression[] { source, selector }), source);
            return orderByExpression.Compile();
        }
        List<T> originalList = new List<T>();
        ListSortDirection sortDirection;
        PropertyDescriptor sortProperty;
        bool isSorted;
        bool ignoreListChanged;
        Func<T, bool> filter;
        public MyBindingList() { }
        public MyBindingList(IEnumerable<T> items) { Update(items); }
        protected override bool SupportsSortingCore { get { return true; } }
        protected override PropertyDescriptor SortPropertyCore { get { return sortProperty; } }
        protected override ListSortDirection SortDirectionCore { get { return sortDirection; } }
        protected override bool IsSortedCore { get { return isSorted; } }
        public Func<T, bool> Filter
        {
            get { return filter; }
            set
            {
                filter = value;
                Refresh();
            }
        }
        public void Update(IEnumerable<T> items)
        {
            originalList.Clear();
            originalList.AddRange(items);
            Refresh();
        }
        public void Refresh()
        {
            var items = originalList.AsEnumerable();
            if (Filter != null)
                items = items.Where(filter);
            if (isSorted)
                items = GetOrderByMethod(sortProperty, sortDirection)(items);

            bool raiseListChangedEvents = RaiseListChangedEvents;
            RaiseListChangedEvents = false;
            base.ClearItems();
            foreach (var item in items)
                Add(item);
            RaiseListChangedEvents = raiseListChangedEvents;
            if (!raiseListChangedEvents) return;
            ignoreListChanged = true;
            ResetBindings();
            ignoreListChanged = false;
        }
        protected override void OnListChanged(ListChangedEventArgs e)
        {
            if (!ignoreListChanged)
                originalList = Items.ToList();
            base.OnListChanged(e);
        }
        protected override void ApplySortCore(PropertyDescriptor prop, ListSortDirection direction)
        {
            var orderByMethod = GetOrderByMethod(prop, direction);
            sortProperty = prop;
            sortDirection = direction;
            isSorted = true;
            Refresh();
        }
        protected override void RemoveSortCore()
        {
            if (!isSorted) return;
            isSorted = false;
            Refresh();
        }
    }
}

which also supports replacing the content and user defined filter.

Here is an example how it can be used, just to get the idea, but I guess you can easily map it to your specific needs:

using System;
using System.Windows.Forms;
namespace Tests
{
    class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    static class Program
    {
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            var form = new Form();
            var dg = new DataGridView { Dock = DockStyle.Fill, Parent = form };
            var filterBox = new TextBox { Dock = DockStyle.Bottom, Parent = form };
            var data = new MyBindingList<Person>(new[]
            {
                new Person { FirstName = "Jon", LastName = "Skeet" },
                new Person { FirstName = "Hans", LastName = "Passant" },
                new Person { FirstName = "Ivan", LastName = "Stoev" },
            });
            dg.DataSource = data;
            var filterText = string.Empty;
            filterBox.TextChanged += (sender, e) =>
            {
                var text = filterBox.Text.Trim();
                if (filterText == text) return;
                filterText = text;
                if (!string.IsNullOrEmpty(filterText))
                    data.Filter = person => person.FirstName.Contains(filterText) || person.LastName.Contains(filterText);
                else
                    data.Filter = null;
            };
            Application.Run(form);
        }
    }
}
Ivan Stoev
  • 159,890
  • 9
  • 211
  • 258
0

You're going to need to reapply the sort each time, because you're applying a whole new datasource.

See if you can adapt something like this:

C# - code against a property using the property name as a string

Community
  • 1
  • 1
Xavier J
  • 4,245
  • 1
  • 11
  • 25