Introduction
I recently came across an article within MSDN that comes bundled with VS.NET 2003: how to make a databindable ListView using VB.NET. I read the article with fascination and hence rolled up my sleeves and got down to creating a C# version, but with even more, such as custom control authoring and esoteric features.
Background
I recently came across a need to display a series of records coming from the database, and was in two minds as to whether the DataGrid
would be more appropriate or a ListView
... I felt at the time that the DataGrid
seemed to be a bit "heavy" in terms of resources and was dismayed to see the lack of databinding support with ListView
. I am still not certain whether the DataGrid
is more resource intensive or not. I'm still debating on this one....please feel free to share your opinions/thoughts on this one, as this would be of invaluable insight, not just to me, but to other CP fans! :-)
This article and this code comes from two sources and I'd like to give credit to them:
Okay, the code can be found in the attachment, study it along with this article, and shall I say, let's get down to the nitty gritty...
What is in this for me?
Ahhhh, okay, let's see:
- Complex data binding.
- The capability to display a string which indicates no data source (read.. when
DataSource
is null
).
- Serializing/deserializing the list to disk.
- Limited threading support! <g>
- Automatically resizing the columns upon deserialization and
- ummmm...the generic standard functionality of an ordinary
ListView
.
- And oh yeah, you can drop this onto a container and adjust/tweak the above settings quite easily! :-)
There, how does that sound? Drooling for this....?
Wow! What a lot, but to be fair, 971 LOC, give or take including/excluding newlines/whitespaces.... By the way, does WinForm generated code count as part of the overall LOC? Food for thought on this one, I'm still wondering... my gut is telling me it ain't! - Answers please anyone! For the uninitiated, LOC is Lines Of Code, a software engineering metrics of measuring complexity of a project. Righto! There may be errors in the code attached as some of it was translated from VB.NET.
Complex databinding
In a nutshell, complex databinding is about supporting all kind of lists of data, i.e. DataSet
type, Array
s, Collection
s. The keyword in the previous sentence is list, or more succinctly, a group of data that implements the underlying IList
or IListSource
interface.
Now, this is something that a majority of developers out there do not need to worry about as it is transparent. I will highlight sections of code that are of interest here, specifically
InnerDataSource
and
GetField
. These two functions are the core of what makes complex databinding possible. In fact, why the hell not, I'll outline the necessary code blocks that can be used to aid you in rolling your own custom-databindable control. So bear with me on this one as I'll piece them together.
private object mDataSource;
private string mDataMember;
private IList InnerDataSource(){
if (this.mDataSource is DataSet){
if (this.mDataMember.Length > 0){
return
((IListSource)((DataSet)this.mDataSource).Tables[this.mDataMember]).GetList();
}else{
return ((IListSource)((DataSet)this.mDataSource).Tables[0]).GetList();
}
}else{
if (this.mDataSource is IListSource){
return ((IListSource)this.mDataSource).GetList();
}else{
return ((IList)this.mDataSource);
}
}
}
Figure 1.
#region GetField Function - Private
private string GetField(object obj, string FieldName){
if (obj is DataRowView){
return (((DataRowView)obj)[FieldName].ToString());
}else{
if (obj is ValueType && obj.GetType().IsPrimitive){
return obj.ToString();
}else{
if (obj is string){
return (string)obj;
}else{
try{
Type SourceType = obj.GetType();
PropertyInfo prop = obj.GetType().GetProperty(FieldName);
if (prop == null || !prop.CanRead){
FieldInfo field = SourceType.GetField(FieldName);
if (field == null){
return "(null)";
}else{
return field.GetValue(obj).ToString();
}
}else{
return prop.GetValue(obj, null).ToString();
}
}catch(Exception){
return "(null)";
}
}
}
}
}
#endregion
Figure 2.
InnerDataSource
(See Figure 1 above)
This function determines if the datasource is of a DataSet
type. If the DataMember
is an empty string, it will return back an IList
type, using the zeroth index to the table entry of the DataSet
. Otherwise, it returns back an IList
type for a table whose name is associated within the DataSet
. That's the complex casting involved as shown. Maybe that's why this is called complex databinding....joke! We get the DataSource
of the aforementioned DataSet
and its associated Tables
property, with an indexer or table name, cast it into an IListSource
, and then finally get the GetList()
method of the IListSource
's implementation which, yup, you've guessed, returns back an IList
implementation!
The other castings are much more simpler. Bottom line to remember is, if mDataSource
belongs to a DataSet
type or DataView
, cast it respectively to get the IListSource
implementation as in the above, then return back the IList
type via the IListSource
's GetList()
method, otherwise, cast it back to an IList
type. Sounds confusing? All types of groups of objects (whether they be string
s, int
s, or even classes), if that grouping implements Array
(or is an Array
object, i.e., SomeType[]
) - it is an IList
, for the rest of 'em - DataSet
, DataView
, or a collection, this is for you!
GetField
(See Figure 2 above)
After translating the original code that was written in VB.NET, my first impression was 'Whoa! What the .... is that?' I just translated. After re-reading that block of code, it made sense to me... can't say how many times I've re-read over a cuppa and a few smokes.... :-) Aye, tis clear as mud! Okay, GetField
simply returns a string
type that is part of the underlying implementation of the IList
interface. We check if the obj
which is of type object
, is one of the following:
DataRowView
- armed with the FieldName
passed into this function, we do a bit more casting to obtain the string value of the field associated with the obj
type via the FieldName
accessor, i.e. [FieldName].ToString()
.
- Primitive type - I use the word primitive type in the loose sense of the word, which can signify the basic .NET fundamental types such as
string
, int
, byte
, char
etc... so we end up using the ToString()
method of that Type or I should say, primitive type.
String
type - that's an easy one....ho hum...
- Finally, last resort, use Reflection to obtain the string contained within the
obj
variable. That's the bit I found "there's a bladder in yer eye" after re-reading it so many times....Reflection...hmmmm.. Okay, the code looks a bit daunting, as to initially I hadn't a clue what was happening there until after re-reading it, consulting MSDN, stepping into it with the VS.NET 2003 debugger, it dawned on me what was happening. For starters, obtain the Reflection Type of the obj
via GetType()
method. That returns a whole bunch of nifty Reflection pieces about this type of obj
. Then we obtain the Reflection's PropertyInfo
to obtain the property associated with the obj
variable's type by using the method GetProperty(FieldName)
. Pause for a moment and think about this scenario. Supposing you have an array of a class type (let's call it SomeClass
for simplicity here), wrapped up in a collection such as an ArrayList
and the code gets executed like this: public class SomeClass{
private string sWotsit = string.Empty;
private int iWotsit = -1;
public SomeClass(string _sWotsit, int _iWotsit){
this.sWotsit = _sWotsit;
this.iWotsit = _iWotsit;
}
public string Foo{
get{ return this.sWotsit; }
}
public int Bar{
get{ return this.iWotsit; }
}
}
private ArrayList arrListSomeClass = new ArrayList();
arrListSomeClass(new SomeClass("Hello There", 5));
With this example, although contrived code in place, let's visualize what's happening with the above code in place regarding Reflection's Type. Reflection will say something like this...."ho...hum, put the type of obj
into this variable SourceType
. FieldName
will contain a string of value "Foo". Okay, .NET, get me the PropertyInfo
for that "Foo" and return back the value into prop
. Let's see if it's null or inaccessible. Okay, it is inaccessible, or it's null, then perhaps, maybe it is a field. Okay, .NET, return back the value from GetField("Foo")
method of SourceType
and shove it into field
. Is the value contents of field
null? It is, I give up. No it isn't, okay return back the value from method GetValue(obj)
of FieldInfo
type. Oh, prop
ain't null. Okay, return back the value from method GetValue(obj, null)
of PropertyInfo
type. And in this contrived code example, since "Foo
" is a property, it would return back a string value, i.e., "Hello There" - sounds contrived, yeah, I feel that's the best way of explaining how Reflection will examine the underlying types. If the code throws an exception or fails, we simply return a string "(null)" since we could not obtain the actual string representation that Reflection failed on!
That's how GetField
works and is a crucial aspect so that .NET's Reflection can figure out what would be the string contents of that particular type by examining it. Quite powerful this Reflection stuff is, eh?
Okay, that's the databinding core covered, what next?
Putting the above together, to determine how to deliver databinding really comes into play here....within this DoAutoDiscovery
function. The only thing missing from this, is what if there's a new row added or a new column (coming from a DataSet
, for instance), how do we get notified of the changes? This method is responsible for setting up the event to listen in on the IBindingList
interface. A IBindingList
implementation is part of the IList
interface and supports the DataView
, and also the DataTable
class. Now, you know why we hook up the event to listen in on any changes... and within the event handler, we simply call DataBind
to rebind the ListView
to the underlying IList
infrastructure.
private IBindingList mBindingList = null;
#region SetSource Method - Private
private void SetSource(){
IList InnerSource = this.InnerDataSource();
if (InnerSource is IBindingList){
this.mBindingList = (IBindingList)InnerSource;
this.mBindingList.ListChanged += new
ListChangedEventHandler(mBindingList_ListChanged);
}else{
this.mBindingList = null;
}
}
#endregion
#region mBindingList_ListChanged Event Handler
private void mBindingList_ListChanged(object sender,
ListChangedEventArgs e) {
this.DataBind();
if (this.Items.Count == 0) this.Invalidate();
}
#endregion
(Figure 3.)
Let's look at how we let the ListView
automatically build up the columns upon binding to a data source, whether it's a DataSet
, Array
or Collection
. The method here is called DoDiscovery
as shown in Figure 4. and is overloaded twice, or another way of saying it, there're two variants of the same method, with different method signatures or parameters. The first method kick starts the discovery of the columns in question and creates a column header for each one discovered within the data source. The first overload is for DataSet
related, the latter is for Array
s, Collection
s. Notice that the Column Headers are our own custom versions inherited from ColumnHeader
class, i.e. mColumns
. It is interesting to see the last overload of DoAutoDiscovery
, for Array
s, Collection
s, there's even more Reflection coming into play here. That's the gist of these methods.
private DataColumnHeaderCollection mColumns = null;
#region DoAutoDiscovery Overloads
#region DoAutoDiscovery Method #1 - Private
private void DoAutoDiscovery(){
if (this.mDataSource == null) return;
IList InnerSource = InnerDataSource();
this.mColumns.Clear();
if (InnerSource == null) return;
this.BeginUpdate();
if (InnerSource is DataView){
DoAutoDiscovery((DataView)InnerSource);
}else{
DoAutoDiscovery(InnerSource);
}
this.EndUpdate();
}
#endregion
#region DoAutoDiscovery Method #2 - Private
private void DoAutoDiscovery(DataView ds){
int Field;
DataColumnHeader Col;
for (Field = 0; Field < ds.Table.Columns.Count; Field++){
if (ds.Table.Columns[Field].ColumnMapping != MappingType.Hidden){
Col = new DataColumnHeader();
Col.Text = ds.Table.Columns[Field].Caption;
Col.Field = ds.Table.Columns[Field].ColumnName;
this.mColumns.Add(Col);
}
}
}
#endregion
#region DoAutoDiscovery Method #3 - Private
private void DoAutoDiscovery(IList ds){
if (ds.Count > 0){
object obj = ds[0];
if (obj is ValueType && obj.GetType().IsPrimitive){
DataColumnHeader Col = new DataColumnHeader();
Col.Text = "Value";
this.mColumns.Add(Col);
}else{
if (obj is string){
DataColumnHeader Col = new DataColumnHeader();
Col.Text = "String";
this.mColumns.Add(Col);
}else{
Type SourceType = obj.GetType();
PropertyInfo[] props = SourceType.GetProperties();
if (props.Length >= 0){
for (int column = 0; column < props.Length; column++){
this.mColumns.Add(props[column].Name);
}
}
FieldInfo[] fields = SourceType.GetFields();
if (fields.Length >= 0){
for (int column = 0; column < fields.Length; column++){
this.mColumns.Add(fields[column].Name);
}
}
}
}
}
}
#endregion
#endregion
Figure 4.
private void DataBinding(){
if (bDisposing) return;
base.Clear();
if (this.mDataSource == null) return;
if (this.mColumns.Count == 0) return;
IList InnerSource = InnerDataSource();
ListViewItem lvi = null;
Cursor current = this.Cursor;
this.Cursor = Cursors.WaitCursor;
this.BeginUpdate();
for (int Field = 0; Field < this.mColumns.Count; Field++){
base.Columns.Add(this.mColumns[Field]);
}
for (int Row = 0; Row < InnerSource.Count; Row++){
lvi = new ListViewItem();
lvi.UseItemStyleForSubItems = this.mUseItemStyleForSubItems;
lvi.Text = this.GetField(InnerSource[Row],
this.mColumns[0].Field).ToString();
for (int Field = 1; Field < this.mColumns.Count; Field++){
lvi.SubItems.Add(this.GetField(InnerSource[Row],
this.mColumns[Field].Field)).ToString();
}
this.Items.Add(lvi);
}
this.EndUpdate();
this.Cursor = current;
}
(Figure 5)
Original credit must go to Rockford Lhotka for his VB.NET version - cheers dude! :-)
Now, databinding out of the way, displaying a message when DataSource is null or Items.Count == 0
Credit's where credit is due to Lubos Haskos & Mav.Northwind for their originality. Here's a block of code that overrides the WndProc
and intercepts the background paint. Throughout the code, there's a quick check in place upon establishing a datasource, i.e., DataSource
within property assignment or when the List changes via the event handler as described in the above Figure 3.. See Figure 6 for the code below...I have instantiated SolidBrush
objects at runtime, using the default colors and have wired up event handlers to adjust the colors of the SolidBrush
objects within the constructor. If the property GridLines
is true
and there's no Items
, or Columns.Count
is zero, it gets temporarily switched off. The code to display a message is executed on a simple check, i.e., if (this.Items.Count == 0) this.Invalidate();
. Yup, that triggers a background paint, i.e., a Windows message WM_ERASEBKGND
gets sent to our overridden WndProc(...)
, that gets intercepted here, then we fill the entire rectangle with our own SolidBrush
background color object, then draw a string via DrawString
method, again, using our own SolidBrush
foreground color. It looks more livelier than the original version in which the named pioneers above Lubos & Mav created, which uses a bland white background with black text.
private string mNoDataMessage = "There are no data available at present.";
private bool mGridLines = false;
private const int WM_ERASEBKGND = 0x14;
private SolidBrush mSbBackColor = new
SolidBrush(System.Drawing.Color.FromKnownColor(KnownColor.Window));
private SolidBrush mSbForeColor = new
SolidBrush(System.Drawing.Color.FromKnownColor(KnownColor.WindowText));
.....
this.BackColorChanged += new EventHandler(DataListView_BackColorChanged);
this.ForeColorChanged += new EventHandler(DataListView_ForeColorChanged);
#region WndProc Override - Protected
protected override void WndProc(ref Message m) {
base.WndProc (ref m);
if (m.Msg == WM_ERASEBKGND){
#region Handle drawing of "no items" message
if (Items.Count == 0 && Columns.Count == 0){
if (this.mGridLines){
base.GridLines = false;
}
using (Graphics g = this.CreateGraphics()) {
using (StringFormat sf = new StringFormat()){
sf.Alignment = StringAlignment.Center;
int w = (this.Width - g.MeasureString(this.mNoDataMessage,
this.Font).ToSize().Width) / 2;
Rectangle rc = new Rectangle(0,
(int)(this.Font.Height*1.5), w, this.Height);
g.FillRectangle(this.mSbBackColor, 0, 0, this.Width, this.Height);
g.DrawString(this.mNoDataMessage,
this.Font, this.mSbForeColor, w, 30);
}
}
}else{
base.GridLines = this.mGridLines;
}
#endregion
}
}
#endregion
#region DataListView_BackColorChanged Event Handler
private void DataListView_BackColorChanged(object sender, EventArgs e) {
this.mSbBackColor.Color = this.BackColor;
}
#endregion
#region DataListView_ForeColorChanged Event Handler
private void DataListView_ForeColorChanged(object sender, EventArgs e) {
this.mSbForeColor.Color = this.ForeColor;
}
#endregion
Figure 6.
Serializing/Deserializing the list to disk.
To serialize/deserialize the items contained in the ListView
to disk, I had to use binary serialization. The advantage of that is smaller data file in comparison to using the XML text equivalent. The call to serialize the ListView
items to disk is just one line that merely states SerializeToDisk(string FileName, bool Overwrite);
. Depending on the thread setting, it will call the private method Serialize2Disk
using the same signature. For deserializing from disk to the ListView
, one merely calls DeSerializeFromDisk(string FileName);
. Simple, and easy to use. Again threading settings apply here also.
To explain, why binary serialization, I had a headache in trying to serialize the items to a plain text XML only to be caught short by the compiler telling me that the ListViewItems
cannot be marked [Serializable()]
. So I resorted to creating a custom class to hold the necessary bits and to do a copy to the custom class' arrays. The custom class in question is called DataListView
.
#region DataLstView Class
[Serializable()]
public class DataLstView{
#region Private Variables within this scope
private ListViewItem[] dlvItemsArr;
private string[] dlvColumnNames;
private byte[] dlvColumnAlignment;
private int[] dlvColumnWidth;
private object[] tagObjectArr;
#endregion
#region DataLstView Constructor - Empty for Serialization
public DataLstView(){}
#endregion
#region DataListViewItems Get/Set Accessor
public ListViewItem[] DataListViewItems{
get{ return this.dlvItemsArr; }
set{ this.dlvItemsArr = value; }
}
#endregion
#region ColumnNames Get/Set Accessor
public string[] ColumnNames{
get{ return this.dlvColumnNames; }
set{ this.dlvColumnNames = value; }
}
#endregion
#region ColumnAlignment Get/Set Accessor
public byte[] ColumnAlignment{
get{ return this.dlvColumnAlignment; }
set{ this.dlvColumnAlignment = value; }
}
#endregion
#region ColumnWidth Get/Set Accessor
public int[] ColumnWidth{
get{ return this.dlvColumnWidth; }
set{ this.dlvColumnWidth = value; }
}
#endregion
#region DataListViewTags Get/Set Accessor
public object[] DataListViewTags{
get{ return this.tagObjectArr; }
set{ this.tagObjectArr = value; }
}
#endregion
}
#endregion
Figure 7.
To enable the serializing to disk, I did a copy to each of DataListView
's arrays via properties as shown in Figure 8. Maybe it is not exactly efficient or code-optimized, but if you know a better way to do this, please feel free to share your ideas here and I will gladly amend this article and code regarding same. :-)
int nItemsCount = this.Items.Count;
if (nItemsCount >= 1){
DataLstView dlvItems = new DataLstView();
dlvItems.DataListViewItems = new ListViewItem[nItemsCount];
dlvItems.DataListViewTags = new object[nItemsCount];
this.Items.CopyTo(dlvItems.DataListViewItems, 0);
dlvItems.ColumnNames = new string[this.Columns.Count];
dlvItems.ColumnAlignment = new byte[this.Columns.Count];
dlvItems.ColumnWidth = new int[this.Columns.Count];
for (int nLoopCnt = 0; nLoopCnt < this.Columns.Count; nLoopCnt++){
dlvItems.ColumnNames[nLoopCnt] = this.Columns[nLoopCnt].Text;
dlvItems.ColumnAlignment[nLoopCnt] =
(byte)this.Columns[nLoopCnt].TextAlign;
dlvItems.ColumnWidth[nLoopCnt] = (int)this.Columns[nLoopCnt].Width;
}
for (int nLoopCnt = 0; nLoopCnt < nItemsCount; nLoopCnt++){
ListViewItem lvi = (ListViewItem)this.Items[nLoopCnt];
dlvItems.DataListViewTags[nLoopCnt] = lvi.Tag;
}
try{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(fs, dlvItems);
}catch(SerializationException){
throw;
}catch(Exception){
throw;
}finally{
fs.Close();
}
}
DataLstView dlvItems = new DataLstView();
this.BeginUpdate();
try{
BinaryFormatter bf = new BinaryFormatter();
this.DataSource = null;
base.Columns.Clear();
this.mColumns.Clear();
this.Items.Clear();
dlvItems = (DataLstView)bf.Deserialize(fs);
if (dlvItems.ColumnNames.Length >= 1){
for (int nLoopCnt = 0;
nLoopCnt < dlvItems.ColumnNames.Length; nLoopCnt++){
base.Columns.Add(dlvItems.ColumnNames[nLoopCnt],
dlvItems.ColumnWidth[nLoopCnt],
(HorizontalAlignment)dlvItems.ColumnAlignment[nLoopCnt]);
this.mColumns.Add(dlvItems.ColumnNames[nLoopCnt],
dlvItems.ColumnWidth[nLoopCnt]);
}
}
base.Items.AddRange(dlvItems.DataListViewItems);
if (dlvItems.DataListViewTags.Length >= 1){
for (int nLoopCnt = 0;
nLoopCnt < dlvItems.DataListViewTags.Length; nLoopCnt++){
ListViewItem lvi = this.Items[nLoopCnt];
lvi.Tag = dlvItems.DataListViewTags[nLoopCnt];
}
}
}catch(SerializationException){
throw;
}catch(Exception){
throw;
}finally{
fs.Close();
this.EndUpdate();
}
Figure 8.
Threading Support
I will include a sample snippet of code which is what threading should be about and how it is accomplished with regards to WinForms. The snippet shows how to perform thread serializing list items to disk. The variables used within the snippets are accessible via Properties Explorer within the Forms Designer, under a custom category aptly called 'Threading'.
I save the parameters into private global variables which can then be accessed via the thread, for this snippet. In the method SerializeToDiskThread
, I check if this.InvokeRequired
is true
. If so, I use the control's BeginInvoke
method using the delegate SerializeToDiskDlgt
. The usage of BeginInvoke
is very well documented, needless to say, using it gives a more snappier feel to the control.
private Thread t_Serialize = null;
private bool mThreadSerialization = false;
#region SerializeToDisk Function
public void SerializeToDisk(string FileName, bool Overwrite){
if (this.mThreadSerialization){
this.mOverwrite = Overwrite;
this.mFilename = FileName;
this.t_Serialize = new Thread(new ThreadStart(SerializeToDiskThread));
this.t_Serialize.Name = "Serializing to Disk Thread";
this.t_Serialize.IsBackground = true;
this.t_Serialize.Start();
}else{
this.Serialize2Disk(FileName, Overwrite);
}
}
#endregion
#region SerializeToDisk Threading
private delegate void SerializeToDiskDlgt(string FileName, bool Overwrite);
private void SerializeToDiskThread(){
lock(this){
if (this.InvokeRequired){
this.BeginInvoke(new SerializeToDiskDlgt(Serialize2Disk),
new object[]{this.mFilename, this.mOverwrite});
}else{
this.Serialize2Disk(this.mFilename, this.mOverwrite);
}
}
}
#endregion
Figure 9.
Automatically resizing the columns upon deserialization
Within the body of the method DeSerializeFromDisk
, a synchronous call is made to this method as shown in Figure 10 below. It wouldn't make a difference if threading is enabled for deserializing from disk or not. The crucial aspect is to measure the string in terms of pixels, not the length of the string, so, we use our dear friend MeasureString
. Basically, there're two things involved, obtain the length of the column header in pixels, then obtaining the largest column text item in pixels for all rows, and checking to see which is the larger value of the two results which are ref
parameters within the methods GetLargestColHdrTextExtent
and GetLargestTextExtent
. I added 8 onto the result as a fudge factor otherwise ellipsis will show either in the column header or in the row itself. Notice the usage of BeginUpdate
and EndUpdate
, that prevents the gawd-awful flickering when the resizing is taking place!
private void ResizeCols(){
Cursor current = this.Cursor;
this.Cursor = Cursors.WaitCursor;
if (this.Items.Count >= 1){
if (this.mColumns.Count >= 1){
this.BeginUpdate();
for (int nLoopCnt = 0; nLoopCnt < this.mColumns.Count; nLoopCnt++){
int nColHdrSize = 0, nColSize = 0;
this.GetLargestColHdrTextExtent(this, nLoopCnt, ref nColHdrSize);
this.GetLargestTextExtent(this, nLoopCnt, ref nColSize);
if (nColHdrSize > nColSize){
this.mColumns[nLoopCnt].Width = nColHdrSize + 8;
}else{
this.mColumns[nLoopCnt].Width = nColSize + 8;
}
nColHdrSize = nColSize = 0;
}
this.EndUpdate();
}
}
this.Cursor = current;
}
Figure 10.
ummmm...the generic standard functionality of an ordinary ListView
And oh yeah, you can drop this onto a container and adjust/tweak the above settings quite easily! :-)
The second biggie after data-binding. How to fit all of this together to form a nifty control that can be dragged onto the WinForms Designer. For starters, to show a pretty nice icon for the toolbox, one must do the following:
- Create a small 16x16 icon.
- Add the icon to the project and set the 'Build Action' to embedded. See Figure 11 below for the screenshot.
Figure 11.
- At the start before
public class DataListView : .....
, add the following, as shown in Figure 12 below: [Serializable(), System.ComponentModel.DesignerCategory("Code"),
ToolboxBitmapAttribute(typeof(<U>TB.nsListViewEx.DataListView</U>),
"DataListView.ico")]
Figure 12.
Be sure you specify the default namespace in the Project Explorer, otherwise the pretty icon won't show in the toolbox and it must match the underlined above!
- Any property that you want to show up in the designer should have the following, as shown in Figure 13:
[System.ComponentModel.Category("Appearance"),
System.ComponentModel.Browsable(<U>true</U>),
// ^^^^ Want the property to be invisible or inaccessible
// thru Properties explorer? Set the underlined to false!
System.ComponentModel.Description("A default message" +
" to show when there is no data bound to this DataListView."),
// ^^^^ The string between quotes will appear
// at the bottom of the Properties Explorer window. Fig 14,B
DefaultValue("There are no data available at present.")]
// ^^^^ That will be the default value
// depending on the property's data type. Fig 14,A
public string UnavailableDataMessage {
get { return this.mNoDataMessage; }
set { if (!value.Equals(this.mNoDataMessage)){
this.mNoDataMessage = value;
Invalidate();
}
}
}
Figure 13.
Figure 14.
- To switch off the property, or not make it appear, or to override it and make it invisible, do this:
[System.ComponentModel.Browsable(false)]
public new System.Windows.Forms.ImageList StateImageList{
get{ return null; }
}
Figure 15.
I have experienced some cases where I developed a custom control and the compiler complains that it could not find a default property even though I had it overridden and made invisible. Even InitializeComponent()
kept insisting on inserting the offending line or property in the code. I ended up manually having to go into the InitializeComponent
and delete the offending line that sets a property for that custom control. I found this snippet will do the trick, change the underlined part to match that of the property name, for instance:
private bool ShouldSerialize<U>StateImageList</U>(){
return true;
}
Figure 16.
If you look in the Forms generated code (within InitializeComponent()
), you would see something like this: dataListView1.StateImageList = null;
. If you were to change the return value to false
, it won't show! And with the trick in place, the compiler accepted that there was no such property and worked. A happy compiler = less hair pulled out = not going bald... for now at least - hey I'll be reaching the big three-oh in a few months... :-) Aside from this trick, it wasn't needed here in this case thankfully, just remember this trick if you experience compiler irritations.
Since this control is a data-binding ListView
, I inherit from System.Windows.Forms.ListView
, i.e.:
public class DataListView : System.Windows.Forms.ListView{
}
Figure 17.
Properties not needed for this data-bindable ListView
:
View
property, this will be defaulted to 'Details
' and made invisible.
MultiSelect
property, this will be defaulted to true
, and made invisible.
FullRowSelect
property, this will be defaulted true
and made invisible.
LargeImageList
, SmallImageList
and StateImageList
properties, turn them off and invisible.
Properties needed:
- To change the
DataSource
, 'Data' category.
- To change the
DataMember
, 'Data' category.
- To change the columns, well actually, the
Columns
property is an override of the inherited ListView
's Column
property and moved to the 'Data' category.
- To enable automatic discovery of column names et al, 'Data' category.
- To be able to use individual
ListView
Item styles, a new property called 'UseItemStyleForSubItems
' under 'Appearance' category.
- To be able to change the message if
DataSource
is null
or no data, 'Appearance' category.
- New category called 'Threading' with the following
bool
values for DataBindThreading
, SerializationThreading
, DeSerializationThreading
and ReSizeColumnsThreading
respectively.
The setting of properties is very similar to the one shown in the above for the creation of a new property, for instance, 'UseItemStyleForSubItems
' which will have a default value of false
. Hey, I'd like to be able to color in the text and change it easily... :-)
[System.ComponentModel.Category("Appearance"),
System.ComponentModel.Browsable(true),
System.ComponentModel.Description("A way of customizing" +
" each column style as per ListViewItem.UseItemStyleForSubItems."),
DefaultValue(false)]
public bool UseItemStyleForSubItems {
get { return this.mUseItemStyleForSubItems; }
set { if (this.mUseItemStyleForSubItems != value){
this.mUseItemStyleForSubItems = value;
}
}
}
Figure 18.
There're two properties that deserve a special mention namely, DataSource
and DataMember
. After all, the reason why, after the translation from VB.NET to this, I realized the significance of the code and hammers home the theme of this article. In a lot of controls that are databindable, there seems to be a common UI within WinForms Designer/Properties Explorer for the two of these properties. I never knew about it until after I somehow 'got lost in translation' (pun intended!).
Have a look at Figure 19 below, there're a few things that seemed alien to me prior to the translation of the original code from VB.NET to C#, and it involves code that I've never seen up to until then.
#region DataSource Property
[System.ComponentModel.Category("Data"),
System.ComponentModel.Browsable(true),
System.ComponentModel.RefreshProperties(RefreshProperties.Repaint),
System.ComponentModel.TypeConverter(
"System.Windows.Forms.Design.<U>DataSourceConverter</U>,
System.Design"),
System.ComponentModel.Description("Data Source.")]
public object DataSource{
get{ return this.mDataSource; }
set{ if (value != null){
this.mDataSource = value;
this.SetSource();
if (this.mAutoDiscovery){
this.DoAutoDiscovery();
if (this.Items.Count == 0) this.Invalidate();
}
this.DataBind();
}
}
}
#endregion
Figure 19.
System.ComponentModel.RefreshProperties
refreshes the Properties Explorer and other properties with any changes made during the selection of the data source. System.ComponentModel.TypeConverter("System.Windows.Forms.Design.DataSourceConverter, System.Design")
: this is responsible for bringing up the standard drop-down window to select the data source.
Figure 20.
The underlined in the above Figure 19, DataSourceConverter
is the key to providing the generic drop-down for providing the list of data sources, the class can be found within System.Design
, which includes DataListMember
which will be covered below.
#region DataMember Property
[System.ComponentModel.Category("Data"),
System.ComponentModel.Editor(
"System.Windows.Forms.Design.DataMemberListEditor,
System.Design", typeof(System.Drawing.Design.UITypeEditor)),
System.ComponentModel.Description("Data Member.")]
public string DataMember{
get{ return this.mDataMember; }
set{ if (value.Length != 0){
this.mDataMember = value;
}
}
}
#endregion
Figure 20.
The above in Figure 20 shows the property code snippet for the DataMember
property. You will notice that I commented out the code to automatically bind the data source. In fact, it is commented out in some places as I found it was data-binding too often and felt it was "slowing down" the loading of the control. Call this my ahem, optimization, look at the constructor and see how I commented out the wiring up of the event handler (Invalidate
) to the Columns
collection class DataColumnHeaderCollection
. Again, feel free to comment on it. The part of this property that intrigues me is the line that says System.ComponentModel.Editor("System.Windows.Forms.Design.DataMemberListEditor,System.Design", typeof(System.Drawing.Design.UITypeEditor))
. Again, you could imagine my reaction when I saw this and spluttered on my cuppa coffee "What the ....". Basically, the property is providing to the WinForms Designer a helluva punch in this single line, a nice generic dropdown window providing different data members for that particular data source. The code tells the designer to use a type of editor to pick out the data members, again, this can be found within System.Design
class. I admit I haven't looked at others yet. As a matter of interest, here's the VB.NET equivalent, just to give you an idea of the translation...
<Category("Data"), _
Editor("System.Windows.Forms.Design.DataMemberListEditor," & _
"System.Design", GetType(System.Drawing.Design.UITypeEditor))> _
Public Property DataMember() As String
Get
Return mDataMember
End Get
Set(ByVal Value As String)
mDataMember = Value
DataBind()
End Set
End Property
After-thoughts
About the demo
I cobbled together a very simple demo which shows and proves the databinding works, build the project and watch it. In the Form1.cs, I manually created a DataSet
and specified this control's DataSource
property and wired it up to this DataSet
. Also, the column names were overridden with custom strings instead of what it would return via Reflection. Look in the btnFill_Click
event handler and notice how I overrode the column names, set its style individually. Also, why not uncomment the code within that handler to use an ArrayList
as its DataSource
: this.CreateAndFillArrList();
to prove it works. Play about with it and let me know how you get on! :-)
Lost in Translations
My biggest stumbling block in the translation of VB.NET code to C#, was this, figuring out the VB.NET's CType(...)
equivalent, for instance, compare the code below in Figure 21 with the above in Figure 1!
Private Function InnerDataSource() As IList
If TypeOf mDataSource Is DataSet Then
If Len(mDataMember) > 0 Then
Return CType(CType(mDataSource, DataSet).Tables(mDataMember), _
IListSource).GetList
Else
Return CType(CType(mDataSource, DataSet).Tables(0), _
IListSource).GetList
End If
ElseIf TypeOf mDataSource Is IListSource Then
Return CType(mDataSource, IListSource).GetList
Else
Return CType(mDataSource, IList)
End If
End Function
Figure 21.
Yes, I did miss out at times on the underscore (VB.NET's line continuation character) when translating/reading VB.NET, which can throw off the translation immediately to the untrained eye.
Why Binary Serialization, but no encryption
With regards to serializing to disk in a binary format fashion, because, my needs were rather specific in the sense that any computer literate person can easily open the XML text and alter the contents of the data within the columns or ListViewItems
. It sounds a bit too contrived in this case, but the threat is there. So to minimize damage limitation, I opted for binary. Sure, anyone can easily open it up with a hex editor... But security is not my forte and henceforth don't fully understand how to encrypt the data. Like, for instance, why choose one security encryption routine implementation over the other and what's the difference etc.?
And that is my own fault (I am a self-taught programmer, had no exposure to the side of programming with security in mind etc.) and of course, it would be my being known as the weakness in the chain of security... so I left out the code to deal with encryption deliberately from the design. Perhaps I'm the idiot here, but I can honestly say 100%, I feel I am just being wise in not going down the road of implementing a more secure way of encrypting the contents, without having a clue on how to do it etc., and am not even confident enough to know which encryption model to use et al.....
Further comments?
I'd like your opinion on this please. Maybe you, the reader might think I'm lazy, irresponsible, naive, paranoid or even stupid in not looking up how to implement a secure encryption approach within MSDN. That's the bit I feel troubled by, the approaches are well publicized, the information is there in the public domain, that computer-literate people can dream up of a way to defeat the encryption just by glancing at the information, and the fact that there're geniuses out there somewhere doing a good job in exposing possible security flaws in every aspect of IT, which all the more makes me slightly nervous in using a well-known encryption methodology.
Serve you well enough?
There, I hope I have made someone's life easier after publishing this article and of course, please feel free to post your thoughts, comments, criticisms, suggestions on how to improve the article/code.
Sl�n leat agus go n�ir� b�thar leat. (Irish saying/expression meaning "Good Luck and may the road rise with you!").