DataSource and BindingList
(by Joe Williams for Blayd Software)
Note: this article relates to the .Net Framework version 2.0, some
of the classes and class members referenced in this article were not available in
earlier framework versions.
Introduction
Data binding in ASP.Net and Windows Forms has been with us for a while and has been
increasing in functionality, flexibility and ease of use with each release of the
.Net Framework. Most of us are familiar with the techniques required to retrieve
data from a database into a DataTable, DataView or DataSet
and there are now a variety of components and controls that are capable of data binding to any of these types.
However, we don't always want to expose these types of data lists directly to user
interface components or controls and sometimes we may not be able to expose them
or indeed the data for our lists may not (originally) come from a database, so these
types may not be the best or easiest type of list structure to use. In many systems,
such as line of business or commercial component or class library systems, there
is a requirement to expose lists of business or server objects to the user interface
tier or client application. In this type of situation we need to provide a list
of business objects from the middle tier of the system or from a server component
to which components or controls hosted in the user interface tier can easily bind.
In this article we going to discuss the development of a server, class library,
project that provides a list of data objects which can be easily data bound to a
user interface component or control. Each data object will expose the properties
of a Windows Service installed on the machine on which the server component is running.
The list will, therefore, contain the details of all of the Windows Services installed
on the machine. To demonstrate two way binding we will make the data objects updatable
and we will allow data objects to be added to and removed from the list. However,
any such changes made by the data bound user interface component or control will
not be reflected back to the actual Windows Service being represented by the data
object. Therefore, the data object may be changed, the list may be changed and the
user interface component or control will update to reflect the changes but the underlying
Windows Service object will not change.
We will show as much of the source code as we have space for, however, we will not
able to show it all. You can download the full source code for this article using the link provided in
the "Article Options" on the left of the page. The source code shown
in this article will be in C# but the download sample code is available as either
a C# or a VB Visual Studio 2005 solution containing two projects, a server component
class library containing the data binding list and a Windows Forms application with
a BindingSource component and a DataGridView control that are used to bind to and
display the list data.
The Data or Business Class that Clients can Bind To
The sample data or business class is named ServiceInfo and it is used to surface
the properties of a single Windows Service. The Windows Service property values
are retrieved using an instance of the System.ServiceProcess.ServiceController class.
An instance of the ServiceInfo class is a simple data object, it does not hold a
reference to the underlying ServiceController and it is not capable of making changes
to the Windows Service that it is representing.
The following ServiceController properties are surfaced by the ServiceInfo class:
- DisplayName
-
The friendly or display name of the service. The
ServiceInfo class permits a data
bound control to update this property.
- MachineName
-
The name of the computer on which the service is running. The
ServiceInfo class
permits a data bound control to update this property.
- ServiceName
-
The unique name of the service. The
ServiceInfo class permits a data bound control
to update this property.
- ServiceType
-
The type of service. The
ServiceInfo class surfaces this property value as a comma
delimited string representation of the set ServiceType enumerated flag bits.
- Status
-
The current status of the service. The
ServiceInfo class surfaces this property
as a string representation the ServiceControllerStatus enumerated value.
To add value to the ServiceInfo class it also surfaces three Windows Service properties
that are not available in the ServiceController class, these properties are as follows:
- Description
-
The description of the service. The
ServiceInfo class permits a data bound control
to update this property.
- LogOnAs
-
The Windows account name under which the service runs. The
ServiceInfo class permits
a data bound control to update this property.
- StartMode
-
The start mode of the service. The
ServiceInfo class surfaces this property as a
string representation of the ServiceStartMode enumerated value. In addition to the
values specified in the ServiceStartMode enumeration, the ServiceInfo class also
provides a string representation of the two extra, device driver only, values of
"Boot" and "System".
Although the ServiceInfo class implements "setters" for some of its properties,
this functionality is only provided to demonstrate two way binding i.e. the updating
of data items by a data bound user interface component or control. Changing the
value of one of the ServiceInfo class properties only updates the data representation
of the Windows Service it does not update the actual service.
The values for the three extra or extended properties are retrieved from the Windows
Registry for each Windows Service during the initialization of each ServiceInfo
instance. As coded in the demonstration source code, the extended properties behave
as expected when running the sample code on a Windows XP machine, however, if you
run the sample code on a Windows Vista machine, the Description property will not
contain the correct data. This is due to differences in the way that the relevant
Registry keys are used on each of the operating systems. Although frustrating, this
issue does not really have any impact on the main purpose of the sample code, so
we resisted the temptation to delve too deeply into the Registry issue and the thought
of producing operating system aware Registry retrieval code in a demonstration project
that does not really have anything to do with the Registry seemed a bit excessive!
Partial listing of the ServiceInfo class:
public class ServiceInfo : INotifyPropertyChanged
{
public ServiceInfo() : base()
{
// ...
}
protected internal ServiceInfo(ServiceController service) : this()
{
// ...
}
[Bindable(false)]
public string MachineName
{
get { return _machineName; }
set
{
_machineName = (string.IsNullOrEmpty(value)) ?
ServiceDataSource.DefaultMachineName : value;
OnPropertyChanged(new PropertyChangedEventArgs("MachineName"));
}
}
public event PropertyChangedEventHandler PropertyChanged;
// ...
}
As mentioned earlier the ServiceInfo class is essentially a fairly simple data object
that represents the properties of a Windows Service, however, there a few points
worthy of further discussion. The class implements the INotifyPropertyChanged interface
in order to notify clients, typically binding clients, that a property value has
changed. The INotifyPropertyChanged interface is contained in the System.ComponentModel
namespace and defines a single member, the PropertyChanged event. When an object,
such as a ServiceInfo class instance, is added to an instance of the BindingList
class, the BindingList examines the object to see if it implements the INotifyPropertyChanged
interface. If it does, the BindingList sinks the PropertyChanged event to enable
it to react and notify its bound clients when one of the objects in its list is
changed. We will be looking at the BindingList class in more detail later in the
article.
The ServiceInfo class implements a default, parameterless, constructor which can
be used to initialize a new instance of the class with default values. We will see
later in the article how the inclusion of a default constructor in its listed objects
influences the behaviour of the BindingList class at runtime. The protected, internal,
constructor is used to initialize a new instance of the ServiceInfo class with the
property values from a ServiceController instance. The ServiceInfo class does not
maintain a reference to the ServiceController passed to the constructor, it just
retrieves the required property values.
We have listed the MachineName property to illustrate several points relating to
properties that can be data bound. The property has a setter, therefore, the ServiceInfo
class is able to participate in two way data binding. The inclusion of a property
setter allows a data bound user interface component or control to update the MachineName
property, although in this particular example updating the property has no affect,
the functionality is provided solely to demonstrate the process. If we allow a property
of a data bound class to be updated, we need to raise the PropertyChanged event
each time that it is updated. Raising the PropertyChanged event notifies all data
bound clients that the ServiceInfo instance has changed, enabling them to update
their display to reflect the change. The MachineName property is decorated with
the BindableAttribute, from the System.ComponentModel namespace and is set to False
to deter user interface designers from listing the property in the default set of
properties to bind to. Clients can still bind to the property, both at design time
and runtime, however, by using the attribute with a value of False we are, at least,
declaring that we would rather clients didn't bind to this property!
The DataSource Component that Provides the Data List
Having developed the ServiceInfo class to provide the properties of a Windows Service
we now have to decide how we are going to present instances of the class to client
applications. An instance of the ServiceInfo class represents a single, installed,
Windows Service and there will be many installed services on most machines, therefore,
we need to provide client applications with a list of ServiceInfo instances to represent
all of the Windows Services installed on the machine. We want to populate and maintain
the service list in the server or business component rather than forcing the client
application developer to write the code to load the service data, that is the main
functional purpose of the server or business component. We do, therefore, have to
provide a populated list of ServiceInfo instances that clients can bind to, however,
placing the code to load the Windows Service data in the list class does not make
much sense, so we need another class to populate and manage the list and expose
the list to client applications. In this kind of situation a data source component
provides an ideal solution, we can include a set of properties that client applications
can use to configure their list requirements, a method or methods that actually
populate the list and a method that enables clients to retrieve an instance of the
populated list and bind to it.
The ServiceDataSource class extends the Component class to provide the developer
of the user interface application with component designer functionality. We want
developers who use our server or business component to be able to specify most,
if not all, of the list configuration options at design time rather than having
to write code.
Partial listing of the ServiceDataSource component:
public partial class ServiceDataSource : Component, IListSource
{
private bool _listServices;
private bool _listDevices;
private string _machineName;
private ServiceBindingList _serviceList;
public ServiceDataSource() : base()
{
// ...
}
public ServiceDataSource(IContainer container) : this()
{
// ...
}
bool IListSource.ContainsListCollection
{
get { return false; }
}
[Browsable(true)]
[Category("Behavior")]
[Description("Specifies whether to list services")]
[DefaultValue(true)]
public bool ListServices
{
get { return _listServices; }
set { _listServices = value; }
}
// ...
protected virtual ServiceBindingList GetServiceListCore()
{
// ...
}
protected virtual void RefreshListCore()
{
// ...
}
public static ServiceController GetController(ServiceInfo info)
{
// ...
}
public ServiceBindingList GetServiceList()
{
if (this.DesignMode)
{
// Return a design time, empty, list...
return new ServiceBindingList();
}
if (_serviceList == null)
{
_serviceList = GetServiceListCore();
}
return _serviceList;
}
System.Collections.IList IListSource.GetList()
{
return GetServiceList() as System.Collections.IList;
}
public void RefreshList()
{
RefreshListCore();
}
}
The ServiceDataSource component class implements the IListSource interface which
is located in the System.ComponentModel namespace and defines two members, the ContainsListCollection
property and the GetList method. The ServiceDataSource component is not itself a
list to which user interface components or controls can bind, however, by implementing
the IListSource interface it defines its purpose as a supplier of a list to which
components or controls can bind. Because it implements the IListSource interface
the ServiceDataSource component can be assigned to the DataSource property of several
user interface components and controls, most notably, from an application developer's
point of view, the System.Windows.Forms.BindingSource component.
The ContainsListCollection property is used to specify whether the list provided
by the ServiceDataSource component is a collection or a collection of collections.
The ServiceDataSource component implementation always returns False because its
list references a collection of objects not a collection of collections. The GetList
method is called by bound clients to retrieve a reference to the list supplied by
the ServiceDataSource component. This method may only be called once by a bound
client if the list implements the IBindingList interface, as thereafter the bound
client can use the IBindingList.ListChanged event to react to changes to the list.
The ServiceDataSource component delegates calls to the IListSource.GetList method
to its own GetServiceList method, casting the returned value to IList.
As you will see in the next section of the article, the list returned by the ServiceDataSource
component does implement the IBindingList interface, located in the
System.ComponentModel
namespace, therefore, the ServiceDataSource component has to assume that bound clients
will sink the IBindingList.ListChanged event. This is the reason why the ServiceDataSource
component only creates and then caches a single instance of its list, rather than
creating a new instance on each call to either the IListSource.GetList or the GetServiceList
method. If it created a new list instance on each call, any event handlers in bound
clients would be referencing the old list instance rather than the newly created
instance and would not, therefore, receive the IBindingList.ListChanged event from
the new instance. Clients can rebuild the entire list by calling the
RefreshList
method which simply clears the ServiceInfo instances from the cached list and then
repopulates it with newly created ServiceInfo instances to reflect the current Windows
Service data.
The ServiceDataSource component implements three properties that client application
developers can use to configure the contents of the service data list. The ListDevices
property, which defaults to False, can be used to include device driver services
in the list and the ListServices property, which defaults to True, can be used to
include normal services in the list. The MachineName property, which defaults to
"." i.e. the local machine, can be used to specify the machine from which to list
Windows Services. The topic of loading Windows Service data from a remote machine
is beyond the scope of this article, however, you should be aware that the sample
code for this article also accesses the Registry to retrieve Windows Service property
data. Therefore, if you specify a remote machine, the code will need to access the
Registry on the remote machine as well as the Windows Service data. The sample code
will do this, providing the necessary protocols are permitted access to the target
machine and the necessary permissions are in place.
All of the ServiceDataSource component's properties can be set in the component
designer, therefore, it is possible for an application developer to utilize the
ServiceDataSource component and display its service data list without having to
write any code.
The BindingList that contains the Data or Business Objects
Data binding in the .Net Framework is, as it should be, interface based i.e. to
provide data that a component or control can bind to, a class has to implement one
or more of the various list and/or data binding interfaces. For complex or list-based
data binding, a class can implement IList or even IEnumerable (requires the BindingSource
component to be used by an application developer), both of these interfaces are
located in the System.Collections namespace. However, for two way data binding with
change notification and sorting, a class should implement the IBindingList interface
from the System.ComponentModel namespace. Implementing the IBindingList interface
can be tricky, however the .Net Framework now contains a concrete, generic, implementation
of IBindingList in the BindingList<T>
class, also located in the System.ComponentModel namespace, which can be considered
as a default implementation of IBindingList. The BindingList base class can be used,
as is, just be specifying the type that is to be contained in the list when creating
an instance, however, by default, BindingList does not support searching or sorting.
Searching may be a "nice to have" for a small list but is likely to be a necessity
for a large list and sorting is, we feel, essential if the list is intended to be
displayed by a user interface control. The DataGridView control, for example, has
built-in support for sorting, if the list that it is bound to supports it, therefore
an application developer gets sorting for free, so may well expect it! The generic
BindingList class is a good starting point for providing a data list that user interface
components and controls can bind to, however, in a lot of situations, you will have
to extend it to provide your own searching and sorting functionality.
For the demonstration Windows Service data list which is the subject of this article
we have extended the BindingList class to develop the ServiceBindingList class,
a data list which supports searching and sorting. For demonstration purposes we
have implemented two way data binding i.e. a ServiceInfo instance contained in the
data list can be updated by a bound component or control and ServiceInfo instances
can be added to or removed from the data list. The BindingList base class contains
three public properties that require further comment, the AllowEdit,
AllowNew and
AllowRemove properties dictate whether items in the list can be edited and whether
items can be added to or removed from the list. All of these properties have setters
with public scope and they can, therefore, be set by a bound component or control.
The AllowNew property defaults to True if the type contained in the list has a default
constructor or if the AddingNew event is handled. If the
AddingNew event is not
handled or if the list type does not have a default constructor, the AllowNew property
defaults to False. You may need to give careful thought to how you implement or
expose these properties in your subclass and whether or not you implement a default
constructor in your list type, if you don't want a bound component or control to
dictate whether or not your data list allows items to be edited, added or removed.
Partial listing of the ServiceBindingList class:
public class ServiceBindingList : BindingList<ServiceInfo>
{
private PropertyDescriptor _sortProperty;
private ListSortDirection _sortDirection;
private bool _isSorted;
private string _nameToMatch;
public ServiceBindingList() : this(new List<ServiceInfo>())
{
// ...
}
public ServiceBindingList(List<ServiceInfo> list) : base(list)
{
// ...
}
protected override bool IsSortedCore
{
get { return _isSorted; }
}
// ...
private bool FindMatch(ServiceInfo item)
{
bool match = false;
if (item != null)
{
match = (string.Compare(_nameToMatch, item.ServiceName,
StringComparison.OrdinalIgnoreCase) == 0);
}
return match;
}
protected override void ApplySortCore(PropertyDescriptor prop,
ListSortDirection direction)
{
if ((prop != null) &&
(typeof(ServiceInfo).IsAssignableFrom(prop.ComponentType)))
{
if (!Enum.IsDefined(typeof(ListSortDirection), direction))
{
// Default to ascending...
direction = ListSortDirection.Ascending;
}
List<ServiceInfo> items = this.Items as List<ServiceInfo>;
if (items != null)
{
IComparer<ServiceInfo> comparer = GetComparer(prop);
if (comparer != null)
{
items.Sort(comparer);
_sortProperty = prop;
_sortDirection = ListSortDirection.Ascending;
if (direction != _sortDirection)
{
items.Reverse();
_sortDirection = direction;
}
_isSorted = true;
OnListChanged(new ListChangedEventArgs(
ListChangedType.Reset, -1));
}
}
}
}
protected override void ClearItems()
{
// ...
}
protected override int FindCore(PropertyDescriptor prop, object key)
{
int index = -1;
_nameToMatch = key as string;
List<ServiceInfo> items = this.Items as List<ServiceInfo>;
if ((items != null) && (!string.IsNullOrEmpty(_nameToMatch)))
{
index = items.FindIndex(this.FindMatch);
}
return index;
}
protected virtual IComparer<ServiceInfo> GetComparer(PropertyDescriptor prop)
{
// ...
}
protected internal virtual PropertyDescriptor GetDefaultSortProperty()
{
// ...
}
protected override void InsertItem(int index, ServiceInfo item)
{
if (item == null)
{
throw new ArgumentNullException("item");
}
base.InsertItem(index, item);
}
protected override void RemoveSortCore()
{
// ...
}
protected override void SetItem(int index, ServiceInfo item)
{
// ...
}
public int IndexOf(string serviceName)
{
// ...
}
}
The ServiceBindingList class extends the generic BindingList<T> class
which extends the generic Collection<T> class from the System.Collections.ObjectModel
namespace. However, neither BindingList<T> or Collection<T>
support searching or sorting but we can pass a list object to the Collection<T> constructor
which it will wrap and use as storage for the list items. Therefore, the ServiceBindingList
constructors either create an empty instance of or accept an instance of the generic
List<T>
class that is then passed, via the BindingList constructor, to the Collection constructor
to be used as storage for the list items. The generic List class does support searching
and sorting, therefore, we can support searching and sorting in the ServiceBindingList
class without having to provide our own searching and sorting algorithms, we can
just delegate searching and sorting to the List instance maintained in the Collection
base class.
There are several protected BindingList properties that we have to override in the
ServiceBindingList class to inform callers that the list supports searching and
sorting, the relevant properties are as follows: IsSortedCore (shown in the listing
above), SortDirectionCore, SortPropertyCore,
SupportsSearchingCore and SupportsSortingCore.
The ServiceBindingList class implements searching by overriding the protected BindingList
FindCore method which returns the index position of the specified list item. The
ServiceBindingList implementation of the FindCore method delegates the search to
the internal List instance maintained in the Collection base class. The method calls
the List FindIndex method specifying the private
ServiceBindingList FindMatch method
as the generic Predicate delegate. The FindMatch method performs a case insensitive
comparison of the ServiceInfo ServiceName property to determine equality, the
ServiceBindingList
only supports searching on the ServiceName property as this is the only Windows
Service property that has to be unique.
Sorting the ServiceBindingList is implemented by overriding the protected
BindingList
ApplySoreCore method, the source code for the override is included in the listing
above. We recommend that you code this method defensively and ignore the call, rather
than throw an exception, if you can't perform the requested sort. This method is
unlikely to be called from user code, it will normally be called by the component
or control that is bound to the list and in our experience exceptions thrown from
this method are often not handled in the calling code and may percolate back into
the runtime, which may cause the client application to be aborted. The
ServiceBindingList
implementation ensures that the PropertyDescriptor is not null
and that it references
either the ServiceInfo class or a subclass of it, the method also implements a default
sort direction if the specified value is invalid and tests all internal references
for null before using them. The method calls the protected virtual
ServiceBindingList
GetComparer method to retrieve the generic IComparer<T> instance required by
the internal List instance to perform the sort. The ServiceBindingList
GetComparer method returns an initialized instance of an internal implementation of the
IComparer<T>
interface which performs a culture aware, case insensitive, comparison of the specified
property value. Finally the ApplySortCore method raises the
ListChanged event with
a value of Reset to notify all bound clients that the list has been rebuilt.
As we have provided an implementation of the ApplySortCore method we also need to
provide an implementation of the RemoveSortCore method. The
RemoveSortCode method
is called from the BindingList implementation of the IBindingList
RemoveSort method.
The documentation for this IBindingList interface method describes its purpose as
follows: "Removes any sort applied using ApplySort". The concept of removing a sort
or un-sorting a list is an interesting one, we can't think of an occasion when we
have required this functionality, however, we are sure that it is possible to do
it if required! The ServiceBindingList override of the protected
BindingList RemoveSortCore
method is a bit of a copout as it just resorts the list using, assumed, default
values, we don't consider it necessary or practical to provide a way of un-sorting
the list.
There are, at least, two important Collection class methods that we need to override
in the ServiceBindingList class to avoid null elements being added to the list.
The protected Collection InsertItem and SetItem methods are overridden to check
that the passed in ServiceInfo is not null. An exception is thrown if an attempt
is made to add or set a null element or the base class implementation is called
if the ServiceInfo parameter is valid.
Using ServiceDataSource and ServiceBindingList in a Client
Application
The ServiceDataSource component can, of course, be used solely from code or an application
developer can bind a user interface control to the component from code, however,
because ServiceDataSource derives from the Component base class, an application
developer can add the ServiceDataSource to a Form and use the component designer
to set its properties and bind it to another component or a data aware control.
Using the ServiceDataSource component and the ServiceBindingList that it provides
at design time is relatively easy for an application developer. In a Windows Forms
project, the developer needs to add, position and size a
DataGridView control on
a Form, then add a BindingSource component and a
ServiceDataSource component. With
the ServiceDataSource component selected, the developer can set any required property
values, although, in most situations the defaults will be fine. With the BindingSource
component selected, assign the ServiceDataSource to the
DataSource property in the
Properties Window. With the DataGridView control selected, assign the
BindingSource
to the DataSource property in the Properties Window. Click the ellipsis button of
the Columns property in the Properties Window and select the
ServiceInfo properties
to bind to in the resulting dialog. The application is now ready to run and will
display, by default, a list of the Windows Services installed on the machine in
the DataGridView control.
Conclusion
The ability to bind one or more user interface controls to data or business objects
singularly or in list form is undoubtedly a great productivity boost for application
developers. However, for component or middle tier developers, coding data objects
and/or data lists to which user interface controls can bind still presents considerable
challenges. For example, how do you present a read only list, with sorting support,
using the existing interfaces available in the .Net Framework? In our experience
this is a fairly common requirement and although it can be done, if you don't like
throwing not supported exceptions from interface members and we don't, it is, perhaps,
not as straight forward as it should be. When and how do you persist changes made
by a bound user interface control, property change notifications are fine in some
situations, however, if a user changes several properties of the same data object,
do you really want to update the data store after each change? The persistence issue
becomes more of a problem if you are using a central data store and the application
has many users. Again, there are ways to implement a sensible persistence strategy,
however, usually this involves adding extra code, which the bound control knows
nothing about, that has to be called by the application developer, such as a Save
method that persists all of the changed properties in a single call to the data
store, for example. Data binding does, however, fit well in some situations and
server or business components that provide it will usually be more popular with
application developers than components that do not. So there is an incentive for
component developers to provide data or business objects that can participate in
data binding, we just need one or two more interfaces to make the process a bit
more intuitive...
Copyright ©Blayd Software Limited 2008-2010. All rights reserved.