Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Extensible ListView a.k.a. DataListView

0.00/5 (No votes)
15 Apr 2005 1  
How to make an extensible ListView custom control, along with data-binding, optional multi-threading and serialization rolled into one.

Empty ExtendedListView

ExtendedListView bound to DataSource

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, Arrays, Collections. 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.

// declarations at start of class pertinent to this block:

private object mDataSource;
private string mDataMember;
//

private IList InnerDataSource(){
 if (this.mDataSource is DataSet){
   if (this.mDataMember.Length > 0){
     // Look ma! Complex Casting here...

     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 strings, ints, 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; }
      }
    }
    // further code....somewhere in some class
    
      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.

// declarations at start of class pertinent to this block:

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);
      // Our event handler here...

  }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 Arrays, Collections. 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 Arrays, Collections, there's even more Reflection coming into play here. That's the gist of these methods.

// declarations at start of class pertinent to this block:

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++){
    // Check if the column within <CODE>DataSet.Tables[...]

    // is hidden! This is intentional!

    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.

// declarations at start of class pertinent to this block:

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));
.....
//within the constructor

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
// Event Handlers

#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. :-)

// within Serialize2Disk method

  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();
      }
    // ....

  }
// Within DeSerializeFromDisk method

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.

// Within the custom control...

private Thread t_Serialize = null;
private bool mThreadSerialization = false;
//...

#region SerializeToDisk Function
public void SerializeToDisk(string FileName, bool Overwrite){
  if (this.mThreadSerialization){ // this property on?

    this.mOverwrite = Overwrite;  // Yup! Thread this please!

    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{                          // Nope! Bog-standard call please!

    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){ // Column Header text is bigger?

          this.mColumns[nLoopCnt].Width = nColHdrSize + 8; // Fudge Factor

        }else{ // Nope!

          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.

    ExtendedListView Build Icon properties

    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!

    ExtendedListView through Project Explorer

  • Any property that you want to show up in the designer should have the following, as shown in Figure 13:
    // Look at Figure 14 for the screenshot
    
    // of Properties explorer for this particular snippet.
    
    [System.ComponentModel.Category("Appearance"), 
    // ^^-- Specify which category in the Properties explorer. Fig. 14,C
    
    
    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.

    ExtendedListView Properties Explorer

    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.

ExtendedListView DataSource within Properties Explorer

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;
         //this.SetSource();

         //if (this.mAutoDiscovery){

         //     this.DoAutoDiscovery();

         //  if (this.Items.Count == 0) this.Invalidate();

         //}

       }
     }
}
#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!

' VB.NET Original version.

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!").

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here