Microsoft Press
Books designed for the different ways you learn. And across the range of Microsoft technologies. Welcome!
We’re pleased to announce that the new Microsoft Press book Developer's Guide to Collections in Microsoft® .NET (ISBN 978-0-7356-5927-8, 646 pages), is now available for purchase!
Written by Calvin (Lee) Janes, a data collection expert, this book is unique because a deep look inside .NET collections: what types of collections are available in the .NET Framework, which ones are most suitable for which types of tasks, working with collection events and interfaces, and managing issues with GUI data binding, threading, data querying, and storage. This book not only provides the most thorough explanation of collection classes in .NET, but also provides task-oriented guidance, exercises, and extensive code samples so you can solve common problems and improve application performance. All code appears in both C# and Microsoft Visual Basic® .
To get a solid feel for the depth of coverage in this book, enjoy Chapter 10, “Using Collections with Windows Form Controls.”
Using Collections with Window Forms
After completing this chapter, you will be able to
· Perform simple bindings with the UI.
· Create two-way bindings with the UI.
· Understand the sample code.
Windows Forms can bind to collections that implement IList and IEnumerable by setting the DataSource property of the ListBox, ComboBox, or DataGridView classes to the collection; the following shows an example.
C#
ListBox lb = new ListBox(); lb.DataSource = new int [] { 1,2,3,4,5 };
Visual Basic
Dim lb As ListBox = New ListBox() lb.DataSource = New Integer() {1, 2, 3, 4, 5}
To control which property these controls should display, you set the DisplayMember property of the ComboBox or ListBox control, as follows.
ComboBox cb = new ComboBox(); cb.DataSource = new Company[] { new Company() { Id = 1, Name = "Alpine Ski House ", Website = "http://www.alpineskihouse.com/" }, new Company() { Id = 2, Name = "Tailspin Toys", Website = "http://www.tailspintoys.com/" } }; cb.DisplayMember = "Name";
Dim cb As ComboBox = New ComboBox() cb.DataSource = New Company() _ { _ New Company() With _ { _ .Id = 1, _ .Name = "Alpine Ski House ", _ .Website = "http://www.alpineskihouse.com/" _ }, _ New Company() With _ { _ .Id = 2, _ .Name = "Tailspin Toys", _ .Website = "http://www.tailspintoys.com/" _ } _ } cb.DisplayMember = "Name"
You can also control which property the controls use for the value of selected item(s) for ComboBox or ListBox controls by using the ValueMember property, as follows.
cb.ValueMember = "Id";
cb.ValueMember = "Id"
When a user selects an item or items, the control’s SelectedValue property then contains the Id property of the selected item(s).
You may have noticed that if you update the collection that you assigned to the DataSource property of a control, the control doesn’t reflect the change. That happens when the data source isn’t a BindingSource or doesn’t implement IBindingList. To support two-way binding, you must set DataSource to an object that implements IBindingList or use the BindingSource.
Note You must perform all Remove, Clear, and Add method calls through BindingSource if you specify a data source that doesn’t implement IBindingList. Otherwise, the user interface (UI) will not see your changes.
IBindlingList provides both simple and complex support for binding to data sources. The interface allows you to notify bound items of list changes and provides support for simple searching and sorting of the underlying collection. You should look at IBindingListView if you need to do complex sorting or add filtering. Microsoft provides BindingList(T) in the .NET Framework so that you do not have to implement an IBindingList from scratch.
Note The sample code in Chapter 10 contains a full implementation of the IBindingList interface called WinFormsBindingList(T). The implementation of each interface implemented by WinFormsBindingList(T) is broken out into separate files to make it easier to follow. You’ll find the implementation of IBindingList in the language-specific files WinFormsBindingList.BindingList.cs and WinFormsBindingList.BindingList.vb. You can review those files for the full implementation of the properties and methods discussed later in this section. Chapters 6 and 8 provide information on how to implement the other interfaces. WinFormsBindingList(T) is the ArrayEx(T) class modified to support the IBindingList interface.
Bound items can determine whether a collection can be modified by using the AllowEdit, AllowNew, and AllowRemove properties, and by using the AddNew method.
public object AddNew() { if (!AllowNew) { throw new InvalidOperationException(); } T retval = Activator.CreateInstance<T>(); Add(retval); m_newIndex = Count – 1; return retval; }
Public Function AddNew() As Object Implements IBindingList.AddNew If (Not AllowNew) Then Throw New InvalidOperationException() End If Dim retval As T = Activator.CreateInstance(Of T)() Add(retval) m_newIndex = Count - 1 Return retval End Function
The field m_newIndex holds the index of the item being added. This is needed so that the bound item, such as the DataGridView, can cancel a newly added item. This is accomplished through the ICancelAddNew interface. The ICancelAddNew interface implementation used in WinFormsBinding(T) is as follows.
public void CancelNew(int itemIndex) { if (m_newIndex.HasValue && m_newIndex.Value == itemIndex) { RemoveAt(itemIndex); } } public void EndNew(int itemIndex) { if (m_newIndex.HasValue && m_newIndex.Value == itemIndex) { m_newIndex = null; } }
Public Sub CancelNew(ByVal itemIndex As Integer) Implements ICancelAddNew.CancelNew If (m_newIndex.HasValue And m_newIndex.Value = itemIndex) Then RemoveAt(itemIndex) End If End Sub Public Sub EndNew(ByVal itemIndex As Integer) Implements ICancelAddNew.EndNew If (m_newIndex.HasValue And m_newIndex.Value = itemIndex) Then m_newIndex = Nothing End If End Sub
If a user starts a new row in the DataGridView and then clicks elsewhere, the DataGridView calls ICancelAddNew.CancelNew; otherwise it calls ICancelAddNew.EndNew after the user enters data in the new row. The CancelNew implementation removes the newly added item from the collection. The EndNew implementation sets m_newIndex to null to denote that the item has been committed.
IBindingList provides list change notifications to items bound to the collection. Bound items can check to see whether the collection sends notifications through the SupportsChangeNotification property, and can receive those notifications through the ListChanged event. Bound items can then inspect the ListChangedEventArgs to see what has changed in the list. Bound items are also notified of property changes to the items in the collection through the ListChanged event.
The collection must raise the ListChanged event whenever the list is modified. This occurs whenever the Add, Clear, Insert, Remove, or RemoveAt methods are called as well as when the Item set property is called. The easiest way to do this is by creating OnXXX and OnXXXComplete methods like those discussed in the “Using CollectionBase” section in Chapter 5, ”Generic and Support Collections.” Each method except the Add method has an OnXXX and OnXXXComplete method, which uses the OnInsert and OnInsertComplete method, and the RemoveAt method, which uses the OnRemove and OnRemoveComplete method. The OnXXX method is called at the beginning of the operation and the OnXXXComplete is called at the end of the method. The following code shows how the code in ArrayEx(T) is modified to support the OnXXX and OnXXXComplete methods.
public void Add(T item) { OnInsert(item, Count); InnerList.Add(item); OnInsertComplete(item, Count - 1); } public void Clear() { var removed = ToArray(); OnClear(removed); InnerList.Clear(); OnClearComplete(removed); } public bool Remove(T item) { if (!AllowRemove) { throw new NotSupportedException(); } int index = IndexOf(item); if (index >= 0) { OnRemove(item, index); InnerList.Remove(item); OnRemoveComplete(item, index); } return index >= 0; } public void RemoveAt(int index) { if (index < 0 || index >= Count) { // Item has already been removed. return; } if (!AllowRemove) { throw new NotSupportedException(); } if (IsSorted) { throw new NotSupportedException("You cannot remove by index on a sorted list."); } T item = m_data[index]; OnRemove(item, index); InnerList.RemoveAt(index); OnRemoveComplete(item, index); } public void Insert(int index, T item) { OnInsert(item, index); InnerList.Insert(index, item); OnInsertComplete(item, index); } public T this[int index] { set { T oldValue = InnerList[index]; OnSet(index, oldValue, value); InnerList[index] = value; OnSetComplete(index, oldValue, value); } }
Public Sub Add(ByVal item As T) Implements ICollection(Of T).Add OnInsert(item, Count) InnerList.Add(item) OnInsertComplete(item, Count - 1) End Sub Public Sub Clear() Implements ICollection(Of T).Clear, IList.Clear Dim removed As T() = ToArray() OnClear(removed) InnerList.Clear() OnClearComplete(removed) End Sub Public Function Remove(ByVal item As T) As Boolean Implements ICollection(Of T).Remove If (Not AllowRemove) Then Throw New NotSupportedException() End If Dim index As Integer = IndexOf(item) If (index >= 0) Then OnRemove(item, index) InnerList.Remove(item) OnRemoveComplete(item, index) End If Return index >= 0 End Function Public Sub RemoveAt(ByVal index As Integer) Implements IList(Of T).RemoveAt, IList.RemoveAt If (index < 0 Or index >= Count) Then ' Item has already been removed. Return End If If (Not AllowRemove) Then Throw New NotSupportedException() End If If (IsSorted) Then Throw New NotSupportedException("You cannot remove by index on a sorted list.") End If Dim item As T = InnerList(index) OnRemove(item, index) InnerList.RemoveAt(index) OnRemoveComplete(item, index) End Sub Public Sub Insert(ByVal index As Integer, ByVal item As T) Implements IList(Of T).Insert OnInsert(item, index) InnerList.Insert(index, item) OnInsertComplete(item, index) End Sub Default Public Property Item(ByVal index As Integer) As T Implements IList(Of T).Item Set(ByVal value As T) Dim oldValue As T = InnerList(index) OnSet(index, oldValue, value) InnerList(index) = value OnSetComplete(index, oldValue, value) End Set End Property
The OnXXX method checks whether the corresponding method can be called, such as when a user calls Insert instead of Add on a sorted list.
void OnSet(int index, T oldValue, T newValue) { if (IsSorted) { throw new NotSupportedException("You cannot set items in a sorted list"); } } void OnClear(T[] itemsRemoved) { } void OnInsert(T item, int index) { // You can only add to the end of the list is sorting is on if (IsSorted && index != Count) { throw new NotSupportedException(); } } void OnRemove(T item, int index) { }
Sub OnSet(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T) If (IsSorted) Then Throw New NotSupportedException("You cannot set items in a sorted list") End If End Sub Sub OnClear(ByVal itemsRemoved As T()) End Sub Sub OnInsert(ByVal item As T, ByVal index As Integer) ' You can only add to the end of the list is sorting is on If (IsSorted And index <> Count) Then Throw New NotSupportedException() End If End Sub Sub OnRemove(ByVal item As T, ByVal index As Integer) End Sub
To keep it simple, the WinFormsBindingList(T) class throws an exception when a user tries to insert an item in a sorted list instead of trying to figure out whether the user should insert the item in the sorted or unsorted list. If you decide to implement insertion into a sorted list in your version, you should determine whether the user wants to insert into the sorted list they currently see or the stored, unsorted list the user sees when she makes a call such as Insert(2,value).
Each one of the OnXXXComplete methods is responsible for notifying the bound item about list changes, registering for property changes, and handling special cases after an add or removal, such as re-indexing or resorting the list.
void OnSetComplete(int index, T oldValue, T newValue) { UnRegisterForPropertyChanges(oldValue); RegisterForPropertyChanges(newValue); if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.ItemChanged, index)); } void OnClearComplete(T[] itemsRemoved) { foreach (var item in itemsRemoved) { UnRegisterForPropertyChanges(item); } if (SupportsSearching) { ReIndex(); } if (m_originalList != null) { m_originalList.Clear(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); } void OnInsertComplete(T item, int index) { RegisterForPropertyChanges(item); if (IsSorted) { Sort(); } else { if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.ItemAdded, index)); } if (m_originalList != null) { m_originalList.Insert(index, item); } } void OnRemoveComplete(T item, int index) { UnRegisterForPropertyChanges(item); if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.ItemDeleted, index)); if (m_originalList != null) { m_originalList.Remove(item); } }
Sub OnSetComplete(ByVal index As Integer, ByVal oldValue As T, ByVal newValue As T) UnRegisterForPropertyChanges(oldValue) RegisterForPropertyChanges(newValue) If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.ItemChanged, index)) End Sub Sub OnClearComplete(ByVal itemsRemoved As T()) For Each item As T In itemsRemoved UnRegisterForPropertyChanges(item) Next If (SupportsSearching) Then ReIndex() End If If (m_originalList IsNot Nothing) Then m_originalList.Clear() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub Sub OnInsertComplete(ByVal item As T, ByVal index As Integer) RegisterForPropertyChanges(item) If (IsSorted) Then Sort() Else If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.ItemAdded, index)) End If If (m_originalList IsNot Nothing) Then m_originalList.Insert(index, item) End If End Sub Sub OnRemoveComplete(ByVal item As T, ByVal index As Integer) UnRegisterForPropertyChanges(item) If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.ItemDeleted, index)) If (m_originalList IsNot Nothing) Then m_originalList.Remove(item) End If End Sub
WinFormsBindingList(T) gets property change notifications by registering to the INotifyPropertyChanged and INotifyPropertyChanging interfaces, as follows.
void UnRegisterForPropertyChanges(T item) { // No need to register for property changes if none of the IBindingList features are supported if ((!m_supportsPropertyChanged && !m_supportsPropertyChanging) || (!SupportsSorting && !SupportsChangeNotification && !SupportsSearching)) { return; } INotifyPropertyChanged changed = item as INotifyPropertyChanged; INotifyPropertyChanging changing = item as INotifyPropertyChanging; if (changing != null && SupportsSearching) { changing.PropertyChanging -= new PropertyChangingEventHandler(T_OnPropertyChanging); } if (changed != null) { changed.PropertyChanged -= new PropertyChangedEventHandler(T_OnPropertyChanged); } } void RegisterForPropertyChanges(T item) { // No need to register for property changes if none of the IBindingList features are supported if ((!m_supportsPropertyChanged && !m_supportsPropertyChanging) || (!SupportsSorting && !SupportsChangeNotification && !SupportsSearching)) { return; } INotifyPropertyChanged changed = item as INotifyPropertyChanged; INotifyPropertyChanging changing = item as INotifyPropertyChanging; if (changing != null && SupportsSearching) { changing.PropertyChanging += new PropertyChangingEventHandler(T_OnPropertyChanging); } if (changed != null) { changed.PropertyChanged += new PropertyChangedEventHandler(T_OnPropertyChanged); } }
Sub UnRegisterForPropertyChanges(ByVal item As T) ' No need to register for property changes if none of the IBindingList features are supported If ((Not m_supportsPropertyChanged And Not m_supportsPropertyChanging) Or (Not SupportsSorting And Not SupportsChangeNotification And Not SupportsSearching)) Then Return End If Dim changed As INotifyPropertyChanged = TryCast(item, INotifyPropertyChanged) Dim changing As INotifyPropertyChanging = TryCast(item, INotifyPropertyChanging) If (changing IsNot Nothing And SupportsSearching) Then RemoveHandler changing.PropertyChanging, New PropertyChangingEventHandler(AddressOf T_OnPropertyChanging) End If If (changed IsNot Nothing) Then RemoveHandler changed.PropertyChanged, New PropertyChangedEventHandler(AddressOf T_OnPropertyChanged) End If End Sub Sub RegisterForPropertyChanges(ByVal item As T) ' No need to register for property changes if none of the IBindingList features are supported If ((Not m_supportsPropertyChanged And Not m_supportsPropertyChanging) Or (Not SupportsSorting And Not SupportsChangeNotification And Not SupportsSearching)) Then Return End If Dim changed As INotifyPropertyChanged = TryCast(item, INotifyPropertyChanged) Dim changing As INotifyPropertyChanging = TryCast(item, INotifyPropertyChanging) If (changing IsNot Nothing And SupportsSearching) Then AddHandler changing.PropertyChanging, New PropertyChangingEventHandler(AddressOf T_OnPropertyChanging) End If If (changed IsNot Nothing) Then AddHandler changed.PropertyChanged, New PropertyChangedEventHandler(AddressOf T_OnPropertyChanged) End If End Sub
Property change notification is required internally for re-indexing and re-sorting the list if the property that changed is being indexed or sorted. Property change notifications are also needed so that bound controls can receive the ListChangeType.ItemChange notification.
Finally, the collection raises the ListChanged event through the OnListChanged method.
void OnListChanged(ListChangedEventArgs e) { if (!SupportsChangeNotification) { return; } if (ListChanged != null) { ListChanged(this, e); } }
Sub OnListChanged(ByVal e As ListChangedEventArgs) If (Not SupportsChangeNotification) Then Return End If RaiseEvent ListChanged(Me, e) End Sub
Notification can be turned off, so the code checks the SupportsChangeNotification flag before raising the ListChanged event.
IBindingList supports sorting through the SupportsSorting, SortDirection, SortProperty, and IsSorted properties as well as the ApplySort and RemoveSort methods. Internally, IBindingList maintains the original list so that it can be restored when sorting is removed. To do this, the Add, Remove, and Clear methods must add to both lists until sorting is removed.
public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { if (!SupportsSorting) { throw new NotSupportedException(); } if (!IsSorted) { if (m_originalList == null) { m_originalList = m_data; m_data = new List<T>(m_originalList); } } m_sortDirection = direction; m_sortDescriptor = property; IsSorted = true; Sort(); }
Public Sub ApplySort(ByVal [property] As PropertyDescriptor, ByVal direction As ListSortDirection) Implements IBindingList.ApplySort If (Not SupportsSorting) Then Throw New NotSupportedException() End If If (Not IsSorted) Then If (m_originalList Is Nothing) Then m_originalList = m_data m_data = New List(Of T)(m_originalList) End If End If m_sortDirection = direction m_sortDescriptor = [property] m_isSorted = True Sort() End Sub
Before a sort operation, the original list is saved and copied over to m_data. The field m_data is then sorted using the Sort method.
void Sort() { if (m_sortDescriptor == null) { return; } m_data.Sort( new LambdaComparer<T> ( (x, y) => { object xValue = m_sortDescriptor.GetValue(x); object yValue = m_sortDescriptor.GetValue(y); if (m_sortDirection == ListSortDirection.Descending) { return System.Collections.Comparer.Default.Compare( xValue, yValue) * -1; } return System.Collections.Comparer.Default.Compare(xValue, yValue); } )); if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Function Compare(ByVal x As T, ByVal y As T) As Integer Dim xValue As Object = m_sortDescriptor.GetValue(x) Dim yValue = m_sortDescriptor.GetValue(y) If (m_sortDirection = ListSortDirection.Descending) Then Return System.Collections.Comparer.Default.Compare(xValue, yValue) * -1 End If Return System.Collections.Comparer.Default.Compare(xValue, yValue) End Function Sub Sort() If (m_sortDescriptor Is Nothing) Then Return End If m_data.Sort(AddressOf Compare) If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub
In the preceding code, the Sort method uses a lambda expression in the Microsoft Visual C# code and a Compare function in the Microsoft Visual Basic code to do the sorting. The lambda expression sort creates the following custom class that implements IComparer(T) and passes the Compare method call to the lambda expression
class LambdaComparer<T> : IComparer<T> { Func<T, T, int> m_compare; public LambdaComparer(Func<T, T, int> compareFunction) { if (compareFunction == null) { throw new ArgumentNullException("compareFunction"); } m_compare = compareFunction; } public int Compare(T x, T y) { return m_compare(x,y); } }
The Sort method then notifies the bound item of the collection change by raising the ListChanged event.
public void RemoveSort() { if (!SupportsSorting) { throw new NotSupportedException(); } m_sortDescriptor = null; IsSorted = false; if (m_originalList != null) { m_data = m_originalList; m_originalList = null; } if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Public Sub RemoveSort() Implements IBindingList.RemoveSort If (Not SupportsSorting) Then Throw New NotSupportedException() End If m_sortDescriptor = Nothing m_isSorted = False If (m_originalList IsNot Nothing) Then m_data = m_originalList m_originalList = Nothing End If If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub
The original list is restored and then indexed. The bound control is then notified of the change.
Searching is supported through the SupportsSearching property and the Find, AddIndex, and RemoveIndex methods. The WinFormsBindingList(T) shows how to get started implementing indexing if your application needs it, but the vast majority of applications do not need to support indexing.
public int Find(PropertyDescriptor property, object key) { if (!SupportsSearching) { throw new NotSupportedException(); } IndexData? data = FindIndexData(property); // See if the property has been indexed if (data.HasValue) { if (data.Value.Indexes.ContainsKey(key)) { var indexes = data.Value.Indexes[key]; if (indexes.Count > 0) { return indexes[0]; } } return -1; } // Find the key by iterating over every element for (int i = 0; i < Count; ++i) { T item = m_data[i]; try { object value = property.GetValue(item); if (System.Collections.Comparer.Default.Compare(value, key) == 0) { return i; } } catch { } } return -1; }
Public Function Find(ByVal [property] As PropertyDescriptor, ByVal key As Object) As Integer Implements IBindingList.Find If (Not SupportsSearching) Then Throw New NotSupportedException() End If Dim data As Nullable(Of IndexData) = FindIndexData([property]) ' See if the property has been indexed If (data.HasValue) Then If (data.Value.Indexes.ContainsKey(key)) Then Dim indexes = data.Value.Indexes(key) If (indexes.Count > 0) Then Return indexes(0) End If End If Return -1 End If ' Find the key by iterating over every element For i As Integer = 0 To Count - 1 Dim item As T = m_data(i) Try Dim value = [property].GetValue(item) If (System.Collections.Comparer.Default.Compare(value, key) = 0) Then Return i End If Catch End Try Next Return -1 End Function
The Find method first checks to see whether the property has been indexed. If it has, it then uses the index table to find the item; otherwise, it performs a linear search for the item.
public void AddIndex(PropertyDescriptor property) { IndexData ?data = FindIndexData(property); if (!data.HasValue) { if (m_indexes == null) { m_indexes = new List<WinFormsBindingList<T>.IndexData>(); } m_indexes.Add ( new IndexData() { Indexes = new Dictionary<object,List<int>>(), PropertyDescriptor = property } ); } ReIndex(); } IndexData ?FindIndexData(PropertyDescriptor property) { if (m_indexes == null) { return null; } foreach (var data in m_indexes) { if (data.PropertyDescriptor == property) { return data; } } return null; }
Public Sub AddIndex(ByVal [property] As PropertyDescriptor) Implements IBindingList.AddIndex Dim data As Nullable(Of IndexData) = FindIndexData([property]) If (Not data.HasValue) Then If (m_indexes Is Nothing) Then m_indexes = New List(Of WinFormsBindingList(Of T).IndexData)() End If m_indexes.Add( _ New IndexData() With { _ .Indexes = New Dictionary(Of Object, List(Of Integer))(), _ .PropertyDescriptor = [property] _ }) End If ReIndex() End Sub Function FindIndexData(ByVal prop As PropertyDescriptor) As Nullable(Of IndexData) If (m_indexes Is Nothing) Then Return Nothing End If For Each data As IndexData In m_indexes If (data.PropertyDescriptor.Name = prop.Name) Then Return data End If Next Return Nothing End Function
The FindIndexData method tries to locate the index that belongs to the specified property. If the property isn’t already indexed, it creates a new index, and then calls the ReIndex method to force a re-indexing of all of the data, as follows.
void ReIndex() { if (m_indexes == null) { return; } // Remove the old indexes foreach (var index in m_indexes) { index.Indexes.Clear(); } if (m_indexes.Count == 0 || !SupportsSearching || !m_canIndex) { return; } // Iterate over each item and add the index to the collection for (int i = 0; i < Count; ++i) { T item = m_data[i]; foreach (var data in m_indexes) { try { object value = data.PropertyDescriptor.GetValue(item); AddIndexData(data, value, i); } catch { } } } }
Sub ReIndex() If (m_indexes Is Nothing) Then Return End If ' Remove the old indexes For Each index As IndexData In m_indexes index.Indexes.Clear() Next If (m_indexes.Count = 0 Or Not SupportsSearching Or Not m_canIndex) Then Return End If ' Iterate over each item and add the index to the collection For i As Integer = 0 To Count - 1 Dim item As T = InnerList(i) For Each Data As IndexData In m_indexes Try Dim value = Data.PropertyDescriptor.GetValue(item) AddIndexData(Data, value, i) Catch End Try Next Next End Sub
ReIndex works by erasing all of the old indexed data and then traversing each item in the collection and indexing that item. As you can tell, this is not an efficient way of indexing the system, but it shows the basic idea behind the AddIndex and RemoveIndex methods.
public void RemoveIndex(PropertyDescriptor property) { if (m_indexes == null) { return; } IndexData? data = FindIndexData(property); if (data.HasValue) { m_indexes.Remove(data.Value); } ReIndex(); }
Public Sub RemoveIndex(ByVal [property] As PropertyDescriptor) Implements IBindingList.RemoveIndex If (m_indexes Is Nothing) Then Return End If Dim data As Nullable(Of IndexData) = FindIndexData([property]) If (data.HasValue) Then m_indexes.Remove(data.Value) End If ReIndex() End Sub
The IBindingListView interface extends the IBindingList interface by providing advanced sorting and filtering capabilities.
Note The sample code in Chapter 10 contains a full implementation of the IBindingListView interface called WinFormsBindingListView(T). The implementation of each interface implemented by WinFormsBindingListView(T) is broken out into separate files to make the logic easier to follow. You’ll find the implementation of IBindingListView in the language-specific files WinFormsBindingListView.BindingListView.cs and WinFormsBindingListView.BindingListView.vb. You can review those files for the full implementation of the properties and methods discussed later in this section. Chapters 6 and 8 describe how to implement the other interfaces. WinFormsBindingListView (T) is the WinFormsBindingList(T) class modified to support the IBindingListView interface. See the “Implementing the IBindingList Interface” section earlier in this chapter for information on implementing IBindingList.
Filtering is accomplished by using the SupportsFiltering and Filter properties as well as the RemoveFilter method.
Filter = [NOT] (PropertyName|[PropertyName]) (>|<|<>|<=|>=|=) (Value|’Value’) [(AND|OR) Filter]
Using that syntax, you can write code such as the following
[Name] = ‘Value’
Name <> Value
Name <= Value AND NOT IsDeleted
NOT IsDeleted
Name = ‘Value’ AND IsDeleted = False
Name = ‘Value’ OR Count = 2
Name = ‘Value’ OR Count = 2 OR Height = 36
The following code shows the implementation in WinFormsBindingListView(T). Because this book is a guide for collections rather than parsing, you won’t be learning the implementation in FilterParser. You can investigate lexer/parsers if you want to implement your own parsing.
public string Filter { get { return m_filter; } set { if (m_filter == value) { return; } m_filterRoot = FilterParser.Parse(value); m_filter = value; if (!string.IsNullOrEmpty(value)) { // Something needs to be filtered if (!m_isFiltering) { ApplyFilter(); } else { ReapplyFilter(); } } else { RemoveFilterInternal(); } } }
Public Property Filter() As String Implements IBindingListView.Filter Get Return m_filter End Get Set(ByVal value As String) If (m_filter = value) Then Return End If m_filterRoot = FilterParser.Parse(value) m_filter = value If (Not String.IsNullOrEmpty(value)) Then ' Something needs to be filtered If (Not m_isFiltering) Then ApplyFilter() Else ReapplyFilter() End If Else RemoveFilterInternal() End If End Set End Property
The Filter property parses the specified string and then determines whether it should apply the filter for the first time, reapply it, or remove the current filter. If the filter is null or empty, the code calls the RemoveFilterInternal method to remove the filter.
void RemoveFilterInternal() { if (!m_isFiltering) { return; } if (IsSorted) { m_data = new List<T>(m_originalList); Sort(); } else { m_data = m_originalList; m_originalList = null; OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); } m_isFiltering = false; }
Sub RemoveFilterInternal() If (Not m_isFiltering) Then Return End If If (IsSorted) Then m_data = New List(Of T)(m_originalList) Sort() Else m_data = m_originalList m_originalList = Nothing OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End If m_isFiltering = False End Sub
The RemoveFilterInternal function restores the original list if the list is currently not being sorted, or it copies the original list and then calls Sort if the list is currently being sorted.
The Filter property calls ApplyFilter if no filter has been applied yet, as follows.
void ApplyFilter() { if (m_isFiltering) { return; } if (m_originalList == null) { m_originalList = m_data; m_data = new List<T>(); } m_isFiltering = true; ReapplyFilter(); }
Sub ApplyFilter() If (m_isFiltering) Then Return End If If (m_originalList Is Nothing) Then m_originalList = m_data m_data = New List(Of T)() End If m_isFiltering = True ReapplyFilter() End Sub
ApplyFilter saves the original list and creates an empty list if the list is currently not being sorted. The empty list is filled in by the ReapplyFilter method.
The ReapplyFilter method traverses the original list and checks the IsFiltered method to see whether the item has been filtered.
bool IsFiltered(T item) { if (m_filterRoot == null) { return false; } return m_filterRoot.Eval(item); } void ReapplyFilter() { if (!IsFiltering) { return; } m_data.Clear(); foreach (T item in m_originalList) { if (IsFiltered(item)) { m_data.Add(item); } } if (IsSorted) { Sort(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Function IsFiltered(ByVal item As T) As Boolean If (m_filterRoot Is Nothing) Then Return False End If Return m_filterRoot.Eval(item) End Function Sub ReapplyFilter() If (Not m_isFiltering) Then Return End If m_data.Clear() For Each item As T In m_originalList If (IsFiltered(item)) Then m_data.Add(item) End If Next If (IsSorted) Then Sort() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub
public void RemoveFilter() { if (!SupportsAdvancedSorting) { throw new NotSupportedException(); } Filter = String.Empty; }
Public Sub RemoveFilter() Implements IBindingListView.RemoveFilter If (Not SupportsAdvancedSorting) Then Throw New NotSupportedException() End If Filter = String.Empty End Sub
You can support advanced sorting through the SupportsAdvancedSorting and SortDescriptions properties as well as the ApplySort method. The ApplySort and RemoveSort methods as well as SortDirection and SortProperty in the IBindingList implementation need to be modified to support both simple and advanced sorting.
public void ApplySort(PropertyDescriptor property, ListSortDirection direction) { if (!SupportsSorting) { throw new NotSupportedException(); } SaveUnsortedList(false); m_sortDescriptors.Clear(); m_sortDescriptors.Add(new ListSortDescription(property,direction)); IsSorted = true; Sort(); }
Public Sub ApplySort(ByVal sorts As ListSortDescriptionCollection) Implements IBindingListView.ApplySort If (Not SupportsAdvancedSorting) Then Throw New NotSupportedException() End If SaveUnsortedList(True) m_sortDescriptors.Clear() For Each sort As ListSortDescription In sorts m_sortDescriptors.Add(sort) Next m_isSorted = True Sort() End Sub
RemoveSort restores the original list if filtering is not enabled, and notifies the bound item of the list change.
public void RemoveSort() { if (!SupportsSorting) { throw new NotSupportedException(); } m_sortDescriptors.Clear(); IsSorted = false; if (m_originalList != null) { m_data = m_originalList; m_originalList = null; } if (IsFiltering) { ReapplyFilter(); } if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Public Sub RemoveSort() Implements IBindingList.RemoveSort If (Not SupportsSorting) Then Throw New NotSupportedException() End If m_sortDescriptors.Clear() m_isSorted = False If (m_originalList IsNot Nothing) Then m_data = m_originalList m_originalList = Nothing End If If (IsFiltering) Then ReapplyFilter() End If If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub
SortDirection and SortPropery return the first item in m_sortDescriptors if m_sortDescriptors contains only one item .
public ListSortDirection SortDirection { get { if (SupportsSorting) { if (m_sortDescriptors.Count == 1) { return m_sortDescriptors[0].SortDirection; } return ListSortDirection.Ascending; } throw new NotSupportedException(); } } public PropertyDescriptor SortProperty { get { if (SupportsSorting) { if (m_sortDescriptors.Count == 1) { return m_sortDescriptors[0].PropertyDescriptor; } return null; } throw new NotSupportedException(); } }
Public ReadOnly Property SortDirection() As ListSortDirection Implements IBindingList.SortDirection Get If (SupportsSorting) Then If (m_sortDescriptors.Count = 1) Then Return m_sortDescriptors(0).SortDirection End If Return ListSortDirection.Ascending End If Throw New NotSupportedException() End Get End Property Public ReadOnly Property SortProperty() As PropertyDescriptor Implements IBindingList.SortProperty Get If (SupportsSorting) Then If (m_sortDescriptors.Count = 1) Then Return m_sortDescriptors(0).PropertyDescriptor End If Return Nothing End If Throw New NotSupportedException() End Get End Property
The Sort method must also be modified to traverse through each item in m_sortDescriptors.
void Sort() { if (m_sortDescriptors.Count <= 0) { return; } m_data.Sort( new LambdaComparer<T> ( (x, y) => { for (int i = 0; i < m_sortDescriptors.Count; ++i) { var sd = m_sortDescriptors[i]; object xValue = sd.PropertyDescriptor.GetValue(x); object yValue = sd.PropertyDescriptor.GetValue(y); int result = 0; if (sd.SortDirection == ListSortDirection.Descending) { result = System.Collections.Comparer.Default.Compare( xValue, yValue) * -1; } else { result = System.Collections.Comparer.Default.Compare( xValue, yValue); } if (result != 0 || i == m_sortDescriptors.Count - 1) { return result; } } System.Diagnostics.Debug.Assert(false); return 0; } )); if (SupportsSearching) { ReIndex(); } OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1)); }
Function Compare(ByVal x As T, ByVal y As T) As Integer For i As Integer = 0 To m_sortDescriptors.Count - 1 Dim sd = m_sortDescriptors(i) Dim xValue As Object = sd.PropertyDescriptor.GetValue(x) Dim yValue = sd.PropertyDescriptor.GetValue(y) Dim result As Integer = 0 If (sd.SortDirection = ListSortDirection.Descending) Then result = System.Collections.Comparer.Default.Compare(xValue, yValue) * -1 Else result = System.Collections.Comparer.Default.Compare(xValue, yValue) End If If (result <> 0 Or i = m_sortDescriptors.Count - 1) Then Return result End If Next System.Diagnostics.Debug.Assert(False) Return 0 End Function Sub Sort() If (m_sortDescriptors.Count <= 0) Then Return End If m_data.Sort(AddressOf Compare) If (SupportsSearching) Then ReIndex() End If OnListChanged(New ListChangedEventArgs(ListChangedType.Reset, -1)) End Sub
AU: I changed the H4 “void ApplySort(ListSortDescirptionCollection sorts)” to just “ApplySort” as we did with long names earlier in the chapter. Change OK?
public void ApplySort(ListSortDescriptionCollection sorts) { if (!SupportsAdvancedSorting) { throw new NotSupportedException(); } SaveUnsortedList(true); m_sortDescriptors.Clear(); foreach (ListSortDescription sort in sorts) { m_sortDescriptors.Add(sort); } IsSorted = true; Sort(); }
Public Sub ApplySort(ByVal [property] As PropertyDescriptor, ByVal direction As ListSortDirection) Implements IBindingList.ApplySort If (Not SupportsSorting) Then Throw New NotSupportedException() End If SaveUnsortedList(False) m_sortDescriptors.Clear() m_sortDescriptors.Add(New ListSortDescription([property], direction)) m_isSorted = True Sort() End Sub
The SaveUnsortedList method saves the original list if the list is not currently being sorted.
void SaveUnsortedList(bool advance) { if (!IsSorted) { if (m_originalList == null) { m_originalList = m_data; m_data = new List<T>(m_originalList); } } }
Sub SaveUnsortedList(ByVal advance As Boolean) If (Not IsSorted) Then If (m_originalList Is Nothing) Then m_originalList = m_data m_data = New List(Of T)(m_originalList) End If End If End Sub
BindingList(T) provides a generic implementation of IBindingList that simplifies the creation of a custom implementation of the IBindingList interface. BindingList(T) implements IBindingList and provides generic support for bounded collections. To override the default implementation of BindingList(T), look for the method or property with the word Core appended to it. For example, to implement RemoveSort, you need to override the RemoveSortCore method. See the section “Implementing the IBindingList Interface” earlier in this chapter for more information on each method and property.
The BindingSource class provides search and sort capability to a data source that implements IBindingList. If the data source you plan to use doesn’t implement IBindingList, and you want bound items to see your changes, you need to call the appropriate methods in the BindingSource class rather than on the data source.
Warning According to the Microsoft documentation at http://msdn.microsoft.com/en-us/library/system.windows.forms.bindingsource(v=VS.100).aspx#Y8235, the DataSource property value of BindingSource should be changed on the UI thread to ensure that the UI reflects the changes.
To use BindingSource, create an instance of the BindingSource class and assign the DataSource property to your collection.
List<int> items = new List<int>(); // Populate items BindingSource source = new BindingSource(); source.DataSource = items;
Dim items As List(Of Integer) = New List(Of Integer)() ' Populate items Dim source As BindingSource = New BindingSource() source.DataSource = items
Then set the UI to the BindingSource as follows.
dataGridView1.DataSource = source;
dataGridView1.DataSource = source
You need to call the Add, Clear, Insert, Remove, and RemoveAt methods in BindingSource instead of the data source if your data source doesn’t implement IBindingList, as follows.
source.Add(1); source.RemoveAt(0);
source.Add(1) source.RemoveAt(0)
The sample project Driver, located in the Samples\Chapter 10 folder, contains examples of using the binding interfaces and objects for collections with a ComboBox, DataGrid, and ListBox.
The ComboBoxBinding form demonstrates how to use the binding classes with a combo box, which is created using the ComboBox class in Windows Forms. An object that implements IBindingList can bind to a combo box by using the following code.
comboBox1.DataSource = m_datasource; comboBox1.DisplayMember = "Name"; comboBox1.ValueMember = "Id";
comboBox1.DataSource = m_datasource comboBox1.DisplayMember = "Name" comboBox1.ValueMember = "Id"
The Update panel shows the currently selected item in the combo box. You can update the selected item by clicking on the Update Item button. The combo box is automatically updated to reflect the updated item. Because each item implements INotifyPropertyChanged, bound controls receive notifications of property changes when items are updated with the following code.
m_showing.Name = NameTextBox.Text; m_showing.Website = WebsiteTextBox.Text;
m_showing.Name = NameTextBox.Text m_showing.Website = WebsiteTextBox.Text
You can use the Add panel to add new items to the combo box. You add items to the combo box by clicking the Add Item button. The combo box is automatically updated to reflect the newly added item. The code for adding an item is as follows.
Company company = new Company(); company.Id = int.Parse(AddIdTextBox.Text); company.Name = AddNameTextBox.Text; company.Website = AddWebsiteTextBox.Text; m_datasource.Add(company);
Dim company As Company = New Company() company.Id = Integer.Parse(AddIdTextBox.Text) company.Name = AddNameTextBox.Text company.Website = AddWebsiteTextBox.Text m_datasource.Add(company)
You can remove the selected item in the combo box by clicking the Remove Item button. The following shows the code for removing an item.
if (comboBox1.SelectedIndex >= 0) { m_datasource.RemoveAt(comboBox1.SelectedIndex); }
If (comboBox1.SelectedIndex >= 0) Then m_datasource.RemoveAt(comboBox1.SelectedIndex) End If
The ListBoxBinding form demonstrates how to use the binding classes with a list box, which is created using the ListBox class in Windows Forms. An object that implements IBindingList can bind to a list box by using the following code.
listBox1.DataSource = m_datasource; listBox1.DisplayMember = "Name";
listBox1.DataSource = m_datasource listBox1.DisplayMember = "Name"
The Update panel shows the currently selected item in the list box. You update the selected item by clicking the Update Item button. The list box is automatically updated to reflect the updated item. Because each item implements INotifyPropertyChanged, bound controls receive notifications of property changes when items are updated with the following code.
You can use the Add panel to add new items to the list box. You add items to the list box by clicking the Add Item button. The list box is automatically updated to reflect the newly added item. The code for adding an item is as follows.
You can remove the selected item in the list box by clicking the Remove Item button. The following shows the code for removing an item.
if (comboBox1.SelectedIndex >= 0) { m_datasource.RemoveAt(listBox1.SelectedIndex); }
If (comboBox1.SelectedIndex >= 0) Then m_datasource.RemoveAt(listBox1.SelectedIndex) End If
The DataGridViewBinding form demonstrates how to use the binding classes with a data grid, which is created using the DatGridView class in Windows Forms. An object that implements IBindingList can bind to a DataGridView by using the following code.
m_datasource = DL.GetDataSource(); dataGridView1.DataSource = m_datasource;
m_datasource = DL.GetDataSource() dataGridView1.DataSource = m_datasource
Items can be added and removed from the data grid by using the same code that is in the ComboBoxBinding and ListBoxBinding forms. In fact, both of the forms use the same data source instance in the sample code as the DataGridViewBinding, so updating the data source in the ComboBoxBinding or ListBoxBinding form also updates the DataGridViewBinding form.
Items can be searched by entering a search string in the search box, selecting a property to search on, and pressing the Search button. A message box will appear stating the row the item was found in. You can also test indexing by selecting the properties you want to index on and then searching on the indexed property. The code for searching is as follows.
if (string.IsNullOrEmpty(SearchTextBox.Text) || SearchPropertyComboBox.SelectedIndex < 0) { return; } var pd = SearchPropertyComboBox.SelectedItem as PropertyDescriptor; int found = -1; try { found = m_datasource.Find(pd, pd.Converter.ConvertFromString(SearchTextBox.Text)); if (found >= 0) { MessageBox.Show(string.Format("Found '{0}' at index {1}", SearchTextBox.Text, found)); } else { MessageBox.Show(string.Format("Didn't find '{0}'", SearchTextBox.Text)); } } catch (Exception ex) { MessageBox.Show(ex.Message); }
If (String.IsNullOrEmpty(SearchTextBox.Text) Or SearchPropertyComboBox.SelectedIndex < 0) Then Return End If Dim pd = TryCast(SearchPropertyComboBox.SelectedItem, PropertyDescriptor) Dim found As Integer = -1 Try found = m_datasource.Find(pd, pd.Converter.ConvertFromString(SearchTextBox.Text)) If (found >= 0) Then MessageBox.Show(String.Format("Found '{0}' at index {1}", SearchTextBox.Text, found)) Else MessageBox.Show(String.Format("Didn't find '{0}'", SearchTextBox.Text)) End If Catch ex As Exception MessageBox.Show(ex.Message) End Try
The DataGridViewAdvanceBinding form is the DataGridViewBinding form with additional UI elements for the IBindingListView interface. The IBindingListView interface allows users to filter the results and sort on multiple columns. The following code shows how to bind IBindingListView object to the data grid control.
m_datasource = DL.GetDataSourceView(); m_binding = New BindingSource(); m_binding.DataSource = m_datasource; dataGridView1.DataSource = m_binding;
m_datasource = DL.GetDataSourceView() m_binding = New BindingSource() m_binding.DataSource = m_datasource dataGridView1.DataSource = m_binding
Users can filter items by entering the filter string into the Filter text box and clicking the Filter button. The following code shows how to filter the IBindingListView object.
m_datasource.Filter = FilterTextBox.Text;
m_datasource.Filter = FilterTextBox.Text
The IBindingListView collection can be sorted on multiple properties by using the Sort property on the BindingSource. To do this in the UI, enter the sort text into the Sort text box, and click the Sort button. The code for doing this is as follows.
m_binding.Sort = SortTextBox.Text;
m_binding.Sort = SortTextBox.Text
The BindingSourceBinding form demonstrates how to perform two-way binding on a collection that doesn’t implement IBindingList. An object that doesn’t implement IBindingList can bind to a list box by using the following code.
m_source = new BindingSource(); m_source.DataSource = new List<Company>(DL.GetData()); listBox1.DataSource = m_source; listBox1.DisplayMember = "Name";
m_source = New BindingSource() m_source.DataSource = New List(Of Company)(DL.GetData()) listBox1.DataSource = m_source listBox1.DisplayMember = "Name"
The Update panel shows the currently selected item in the list box. You can update the selected item by clicking the Update Item button. The list box is automatically updated to reflect the updated item. Because each item implements INotifyPropertyChanged, items are updated with the following code.
You can use the Add panel to add new items to the list box. You add items to the list box by clicking the Add Item button. The list box updates automatically to reflect the newly added item. The item needs to be added using the BindingSource instead of the List(T) for the list box to see the changes, because List(T) doesn’t implement IBindingList.
Company company = new Company(); company.Id = int.Parse(AddIdTextBox.Text); company.Name = AddNameTextBox.Text; company.Website = AddWebsiteTextBox.Text; m_source.Add(company);
Dim company As Company = New Company() company.Id = Integer.Parse(AddIdTextBox.Text) company.Name = AddNameTextBox.Text company.Website = AddWebsiteTextBox.Text m_source.Add(company)
You can remove the selected item in the list box by clicking the Remove Item button. The following shows the code for removing an item. The item needs to be removed using the BindingSource instead of the List(T) for the list box to see the changes, because List(T) doesn’t implement IBindingList.
if (comboBox1.SelectedIndex >= 0) { m_source.RemoveAt(listBox1.SelectedIndex); }
If (comboBox1.SelectedIndex >= 0) Then m_source.RemoveAt(listBox1.SelectedIndex) End If
In this chapter, you saw how to bind collections to controls used in Windows Forms. You saw how to have two-way bound controls by using the IBindingList interface. You also saw that you can use the IBindingList interface to sort and search, and use the IBindingLIstView interface to do advanced filtering and sorting.