So far in this book you have seen the use of a TreeView control and a ListView control in separate examples. What I have shown you is the tip of the iceberg as far as these controls go. Making full use of all the capabilities of the TreeView and ListView controls is the secret to a really good object-oriented GUI.
Here are some things you have not seen with these controls that, when used, really spice up a user interface:
Multiple levels in the TreeView
Pictures in TreeView nodes and ListView cells
Drag-and-drop capability in the TreeView and ListView
Node manipulation in the TreeView
Cell manipulation in the ListView
Different ListView views
Virtual nodes and views
Background and foreground color manipulation
Getting to know the TreeView control and how to use it effectively is really worthwhile. Now, you can consult the online help for TreeView and ListView examples, but you will find them lacking in cohesiveness. What you need to know is how all these items work together to provide a complete data entry and manipulation screen. This is what I show you in the next section.
The ListView control allows you to present data in one of four ways:
Large icons
Small icons
Small icons in a vertical list
Report view
You see the ListView control all the time when you use a computer. The right pane in Windows Explorer is a ListView.
If you are a VB 6.0 veteran, you will have undoubtedly used the ListView. The .NET version is somewhat different in its setup, however, and requires familiarity with collections and how they work.[2]
The ListView consists of the visible control, which includes many methods and properties. To add any data to this control, you need to add records to an ItemList collection. There is a method in the ListView class that you can use to add items directly to the list. There is also a method to add an array of ListViewItems to the control. Here is how to do it.
C#
ListView lvw = new ListView(); lvw.Items.Add("First Item"); lvw.Items.Add( new ListViewItem("Second Item")); ListViewItem[] items = new ListViewItem[3] { new ListViewItem("fourth Item"), new ListViewItem("fifth Item"), new ListViewItem("sixth Item")}; lvw.Items.AddRange(items);
VB
Dim lvw As ListView = New ListView() lvw.Items.Add("First Item") lvw.Items.Add(New ListViewItem("Second Item")) Dim items() As ListViewItem = New ListViewItem(2) { _ New ListViewItem("fourth Item"), _ New ListViewItem("fifth Item"), _ New ListViewItem("sixth Item")} lvw.Items.AddRange(items)
This code shows the various ways to add items to a ListView. I have found, however, that I often want to keep several sets of data that I can swap in and out of the ListView. For instance, I may want to show a list of all 1,000 people in the San Francisco office. I may also want to instantly switch the view with a list of all 870 people in the Boston office. Constantly going back and forth to the database, creating ListItems, and adding them to the ListView can be tedious and slow.
In the next example, you will create a class that allows you to swap a ListView list with another one. The output may not be very jazzy, but this is the first step toward a more involved and more graphical data entry program than you have seen thus far in the book.
Start a new project in either C# or VB. Mine is called "ListViewSwap."
Add a ListView control named lv to the form.
Add a Button named cmdSwap to the form. Change its text to Swap.
Make sure the form starts in the center of the screen.
Figure 5-6 shows what this form should look like.
Now for the ListView collection class. Add a new class called "LvItems" to the project. Listings 5-1a and 5-1b show the code for this class.
![]() |
using System; using System.Windows.Forms; using System.Collections; namespace ListViewSwap_c { /// <summary> /// Summary description for LvItems. /// </summary> public class LvItems { private ArrayList mCol; public LvItems() { mCol = new ArrayList(); } /// <summary> /// Add a string to an array of ListViewItems /// </summary> /// <param name="val"></param> public void Add(string val) { mCol.Add(new ListViewItem(val)); } /// <summary> /// Add a ListViewItem to an array of ListViewItems /// </summary> /// <param name="val"></param> public void Add(ListViewItem val) { mCol.Add(val); } /// <summary> /// Gets a ListViewItem array /// </summary> public ListViewItem[] Items { get { mCol.TrimToSize(); ListViewItem[] lvw = new ListViewItem[mCol.Count]; mCol.CopyTo(lvw, 0); return lvw; } } } }
![]() |
![]() |
Option Strict On Imports System Imports System.Windows.Forms Imports System.Collections Public Class LvItems Private mCol As ArrayList Public Sub New() mCol = New ArrayList() End Sub ' <summary> ' Add a string to an array of ListViewItems ' </summary> ' <param name="val"></param> Public Sub Add(ByVal val As String) mCol.Add(New ListViewItem(val)) End Sub ' <summary> ' Add a ListViewItem to an array of ListViewItems ' </summary> ' <param name="val"></param> Public Sub Add(ByVal val As ListViewItem) mCol.Add(val) End Sub ' <summary> ' Gets a ListViewItem array ' </summary> Public ReadOnly Property Items() As ListViewItem() Get mCol.TrimToSize() Dim lvw(mCol.Count - 1) As ListViewItem mCol.CopyTo(lvw, 0) Return lvw End Get End Property End Class
![]() |
I called this a collection class. It is missing a few methods, however. Strictly speaking, a collection class needs a way to enumerate over its internal list. It also should have a remove method and way to get a single item as well. I have not put these methods in here because they are not needed for my purposes.
The reason for using this class as opposed to just a generic array or collection is type safety. Using this class, it is impossible to hold anything other than a ListViewItem. A normal ArrayList or generic collection can hold any number of any data type. It is best to enforce type safety whenever you can.
Now it is time to add the code for the main form. First, add some local variables.
C#
#region class local variables LvItems lvw1; LvItems lvw2; LvItems SwapList; #endregion
#Region "class local variables" Private lvw1 As LvItems Private lvw2 As LvItems Private SwapList As LvItems #End Region
You can probably guess what the code will do from looking at the variables. I instantiate two LvItems objects to hold different sets of data. The swap list is what I will display in the ListView. The constructor for this form follows.
C#
public Form1() { InitializeComponent(); //Show details with two columns lv.View = View.Details; lv.Columns.Add("First Item", -2, HorizontalAlignment.Center); lv.Columns.Add("Sub Item", -2, HorizontalAlignment.Center); //Add individual items to the first stored list lvw1 = new LvItems(); lvw1.Add("1 I belong to LC1"); lvw1.Add("2 I belong to LC1"); lvw1.Add("3 I belong to LC1"); //Add an item to the first list that has a subitem ListViewItem k = new ListViewItem(); k.Text = "4 Parent Item"; k.SubItems.Add("Sub Item 1"); lvw1.Add(k); //Add items to the second stored list lvw2 = new LvItems(); lvw2.Add("1 I belong to LC2"); lvw2.Add("2 I belong to LC2"); lvw2.Add("3 I belong to LC2"); lv.Items.AddRange(lvw1.Items); }
Public Sub New() MyBase.New() InitializeComponent() 'Show details with two columns lv.View = View.Details lv.Columns.Add("First Item", -2, HorizontalAlignment.Center) lv.Columns.Add("Sub Item", -2, HorizontalAlignment.Center) 'Add individual items to the first stored list lvw1 = New LvItems() lvw1.Add("1 I belong to LC1") lvw1.Add("2 I belong to LC1") lvw1.Add("3 I belong to LC1") 'Add an item to the first list that has a subitem Dim k As ListViewItem = New ListViewItem() k.Text = "4 Parent Item" k.SubItems.Add("Sub Item 1") lvw1.Add(k) 'Add items to the second stored list lvw2 = New LvItems() lvw2.Add("1 I belong to LC2") lvw2.Add("2 I belong to LC2") lvw2.Add("3 I belong to LC2") lv.Items.AddRange(lvw1.Items) End Sub
I set up the list to have two columns. I then add some records to each of the LvItems objects. I even add a ListViewItem that has a subitem just to show that this is possible as well.
The last line in this constructor goes out to the ListViewItem collection object and retrieves an array of ListViewItems that are then added to the list. If I want to swap this list out for another, I need a little more code. This code is included in the following delegate. I got this delegate definition generated automatically for me by double-clicking on the cmdSwap button on the form.
private void cmdSwap_Click(object sender, System.EventArgs e) { SwapList = (SwapList == lvw2) ? lvw1 : lvw2; lv.Items.Clear(); lv.Items.AddRange(SwapList.Items); }
VB
Private Sub cmdSwap_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdSwap.Click If SwapList Is lvw2 Then SwapList = lvw1 Else SwapList = lvw2 End If lv.Items.Clear() lv.Items.AddRange(SwapList.Items) End Sub
What I do here is reference a different list than the one contained in the SwapList. I then clear the ListView control and retrieve the new array of ListViewItems. Try this code out.
There is not much here and it has little "wow" factor. However, this method of storing ListViewItems and swapping them out with existing ones in a control will be very important. Think about it a little and you will find that you can adapt this kind of class to store any kind of type-safe list.
In the next section you'll move on to more involved uses for the ListView.
In this section I present an example that uses the ListView control in several different ways. I am not going to fall into the usual trap of supplying just a few short lines of setup code, however.
Creating different views of the ListView control also necessitates creating different uses. Perhaps a detail view would be used one way, and a large icon view would be used another way. It is how you use the different views that makes this control so flexible, not just how you display the data.
This section's example is a demonstration of how you could use a ListView control in a movie rental program. Quite a bit of the example's functionality is behind the scenes. You will notice that as you run the program, you are not required to use the keyboard for any data entry. You do it all with the mouse. Here is what you will see in this example:
A ListView in Detail mode
A ListView in LargeIcon mode
Getting data from a file on disk
Storing data as objects and collections of objects
Manipulating objects via mouse actions
The drag-and-drop capability of the TreeView and PictureBox
Using the ComboBox effectively in an object-oriented way
Along the way, I point out some important details that you can use in your future code.
Start a new C# or VB project. Mine is called "MovieList."
Note |
This example uses a few text and image files. If you download the code for this example from the Downloads section of the Apress Web site (http://www.apress.com), you will get these files. If you decide not to enter the code by hand (entering by hand is recommended), at least follow the code description. There are some concepts introduced here that are crucial to understanding how this program works. These concepts will be very useful in any future .NET programming you do. |
You will need to make the following changes to the main form. Figure 5-7 shows what the form should look like.
Size the form to be 700×450. Start-up should be Center Screen.
Add a ComboBox below the Label called cmbGenre.
Add a Label that reads Rentals. Center the text within the Label.
Add a ListView control below the Rentals Label called lstRentals.
Add a Label that reads For Sale. Center the text within the Label.
Add a ListView control below the For Sale Label called lstSold.
Add a PictureBox called picRental.
Add a Label below the PictureBox that reads Rental Check Out.
This form is not anything that I would sell as a rental package. It is intended to show how the ListView controls work and how all of these controls can interact with each other.
What would an object-oriented program be without objects? You will need to add three additional classes to make this program work. Add the following classes to your project:
To rent movies, this program will need a Movie object. This object holds only a few items of information, such as the movie title and the rental price. A real program would need a whole host of properties. The code for this class follows.
C#
using System; namespace MovieList_c { /// <summary> /// Summary description for Movie. /// </summary> public class Movie { private string mName; private Decimal mSalePrice; private Decimal mRentalPrice; public Movie(string name) { mName = name; mSalePrice = 12.95m; mRentalPrice = 3.40m; } public string Name { get { return mName; } } public Decimal SalePrice { get { return mSalePrice; } } public Decimal RentalPrice { get { return mRentalPrice; } } } }
VB
Option Strict On Public Class Movie Private mName As String Private mSalePrice As Decimal Private mRentalPrice As Decimal Public Sub New(ByVal name As String) mName = name mSalePrice = 12.95D mRentalPrice = 3.4D End Sub Public ReadOnly Property Name() As String Get Return mName End Get End Property Public ReadOnly Property SalePrice() As Decimal Get Return mSalePrice End Get End Property Public ReadOnly Property RentalPrice() As Decimal Get Return mRentalPrice End Get End Property End Class
Now that you have a Movie object, what do you do with it? You could put it in a collection of movies, but that would mean that at some point you would need to enumerate over that movie collection to find the one you want. This works but it requires code.
In thinking about the design of this example, I knew that I would be working with ListViewItems as my major object. After all, I have two ListView controls that work only with ListViewItems as objects. A ListViewItems has a Tag property. This Tag property was designed to hold an object whose data relates to the ListViewItem. In the case of this example, I store the Movie object in the ListViewItem.Tag property of each record shown in the ListView control. This gives me direct access to the movie in question without having to search through some unrelated collection.
The next class to fill in is the MovieList class. This class does several things. The basis of the class is the same as the class I showed you in the previous example; it is designed to hold a collection of ListViewItem objects. I have added the following functionality to this class:
Parses each line of the text file into a ListViewItem with associated subitems
Instantiates a Movie object for each ListViewItem and saves it in the ListViewItem.Tag property
Removes a ListViewItem from the collection
Changes a property of a single ListViewItem in the collection
The code in Listings 5-2a and 5-2b show this MovieList class. Read the code carefully to see what I am doing.
![]() |
using System; using System.IO; using System.Collections; using System.Windows.Forms; namespace MovieList_c { /// <summary> /// Summary description for MovieList. /// </summary> public class MovieList { private const string OUT = "Out"; private const string INHOUSE = "In"; private ArrayList mCol; private char mDelimiter = '^'; private string mGenre; public MovieList() { mCol = new ArrayList(); } public MovieList(string fname) { string buffer; mCol = new ArrayList(); FileInfo fIn = new FileInfo(fname); try { StreamReader sr = new StreamReader( fIn.OpenRead() ); while (sr.Peek() != -1) { buffer = sr.ReadLine(); string[] List = buffer.Split(mDelimiter); if(List.GetLength(0) == 1) { if(buffer != string.Empty) mGenre = buffer; } else if(List.GetLength(0) > 1) { ListViewItem l = new ListViewItem(List); l.Tag = new Movie(l.Text); mCol.Add(l); } } sr.Close(); } catch(Exception e) { MessageBox.Show("Unable to read file " + fname); throw e; } } public override string ToString() { return mGenre; } public string Genre { get{return mGenre;} } /// <summary> /// Add a string to an array of ListViewItems /// </summary> /// <param name="val"></param> public void Add(string val) { mCol.Add(new ListViewItem(val)); } /// <summary> /// Add a ListViewItem to an array of ListViewItems /// </summary> /// <param name="val"></param> public void Add(ListViewItem val) { mCol.Add(val); } /// <summary> /// Remove a ListViewItem from the array of ListViewItems /// </summary> /// <param name="val"></param> public void Remove(ListViewItem val) { mCol.Remove(val); } /// <summary> /// Checkout a movie /// </summary> /// <param name="val"></param> public bool CheckOut(ListViewItem val) { string stock = val.SubItems[val.SubItems.Count-1].Text; if(stock == OUT) return false; mCol.Remove(val); val.SubItems[val.SubItems.Count-1].Text = OUT; mCol.Add(val); return true; } /// <summary> /// Gets a ListViewItem array /// </summary> public ListViewItem[] Items { get { mCol.TrimToSize(); ListViewItem[] lvw = new ListViewItem[mCol.Count]; mCol.CopyTo(lvw, 0); return lvw; } } } }
![]() |
![]() |
Option Strict On Imports System Imports System.IO Imports System.Collections Imports System.Windows.Forms Public Class MovieList Private Const OUT As String = "Out" Private Const INHOUSE As String = "In" Private mCol As ArrayList Private mDelimiter As Char = "^"c Private mGenre As String Public Sub New() mCol = New ArrayList() End Sub Public Sub New(ByVal fname As String) Dim buffer As String mCol = New ArrayList() Dim fIn As FileInfo = New FileInfo(fname) Try Dim sr As StreamReader = New StreamReader(fIn.OpenRead()) While (sr.Peek() <> -1) buffer = sr.ReadLine() Dim List() As String = buffer.Split(mDelimiter) If List.GetLength(0) = 1 Then If (buffer <> String.Empty) Then mGenre = buffer End If ElseIf List.GetLength(0) > 1 Then Dim l As ListViewItem = New ListViewItem(List) l.Tag = New Movie(l.Text) mCol.Add(l) End If End While sr.Close() Catch e As Exception MessageBox.Show("Unable to read file " + fname) Throw e End Try End Sub Public Overrides Function ToString() As String Return mGenre End Function Public ReadOnly Property Genre() As String Get Return mGenre End Get End Property Public Sub Add(ByVal val As String) mCol.Add(New ListViewItem(val)) End Sub Public Sub Add(ByVal val As ListViewItem) mCol.Add(val) End Sub Public Sub Remove(ByVal val As ListViewItem) mCol.Remove(val) End Sub Public Function CheckOut(ByVal val As ListViewItem) As Boolean Dim stock As String = val.SubItems(val.SubItems.Count - 1).Text If stock = OUT Then Return False End If mCol.Remove(val) val.SubItems(val.SubItems.Count - 1).Text = OUT mCol.Add(val) Return True End Function Public ReadOnly Property Items() As ListViewItem() Get mCol.TrimToSize() Dim lvw(mCol.Count) As ListViewItem mCol.CopyTo(lvw, 0) Return lvw End Get End Property End Class
![]() |
Let's dissect this class a little. Look at the constructor. Inside of a Try-Catch block I open a text file, read it in line by line, and create ListItems from the contents of each line. Here is the relevant C# code:
StreamReader sr = new StreamReader( fIn.OpenRead() ); while (sr.Peek() != -1) { buffer = sr.ReadLine(); string[] List = buffer.Split(mDelimiter); if(List.GetLength(0) == 1) { if(buffer != string.Empty) mGenre = buffer; } else if(List.GetLength(0) > 1) { ListViewItem l = new ListViewItem(List); l.Tag = new Movie(l.Text); mCol.Add(l); } }
At the top of this file I declared a delimiter constant to be the caret symbol. If you look through the file, you will see that each line uses this symbol for a delimiter. Here is the ActionMovie.txt file:
Action Movies StageCoach^1932^94^VHS^In Speed^1994^120^8mm^Out Alien^1986^98^DVD^In Train Robber^1923^36^VHS^In
The first line of this file is the genre of movies. The code detects this because there are no delimiters in this line. The other four lines are delimited records. As each one is read in, I create an array of strings to represent each field in the record. I then add each array en masse to the constructor of the ListViewItem class, which automatically creates a ListViewItem and as many ListViewSubItem objects as needed. This ListViewItem is then added to the total collection. The first line I read in gets assigned to the Genre variable.
There is one other line of code in this class that I would like to draw your attention to. Here is the C# code:
public override string ToString() { return mGenre; }
Would you believe me if I told you that without this line of code, the whole program would not work as it does? You will learn the reason for this in a bit. For now, see the sidebar "Overriding the ToString() Method" for an explanation.
I will leave the third class, ListViewSorter, for a bit later.
You have the supporting guts of this project done. Now it is time to add the code for the main form. Start out with some class local variables.
C#
#region class local variables MovieList ActionMovies; MovieList DramaMovies; MovieList ComedyMovies; ImageList BigIcons; #endregion
VB
#Region "class local variables" Private ActionMovies As MovieList Private DramaMovies As MovieList Private ComedyMovies As MovieList Private BigIcons As ImageList #End Region
As you can see, I have three types of movies. I also have an ImageList that I use for the icon display ListView control. Before I add any constructor code I will need some delegates to call. The mouse does a lot of work in this program, so I have quite a few mouse event handlers. Most of them are used for drag-and-drop operations.
C#
#region Delegates private void GenreClick(object sender, EventArgs e) { ComboBox cmb = (ComboBox)sender; GetList((MovieList)cmb.SelectedItem); } private void MovieRentalStartDrag(object sender, ItemDragEventArgs e) { lstRentals.DoDragDrop(e.Item, DragDropEffects.Move); } private void MovieRentalDragAcross(object sender, DragEventArgs e) { e.Effect = DragDropEffects.Move; } private void MovieDragInto(object sender, DragEventArgs e) { //The data must come from the lstRentals ListView object o = e.Data.GetData(DataFormats.Serializable); if(o is ListViewItem) { ListViewItem l = (ListViewItem)o; if (l.ListView == lstRentals) e.Effect = DragDropEffects.All; else e.Effect = DragDropEffects.None; } } private void RentalCartDrop(object sender, DragEventArgs e) { object o = e.Data.GetData(DataFormats.Serializable); ListViewItem l = (ListViewItem)o; DialogResult dr = MessageBox.Show("Confirm Rental of " + l.Text, "Rent Video", MessageBoxButtons.YesNo); if(dr == DialogResult.No) return; //Look at the genre combo box to see which movie list to delete this //ListViewItem from. MovieList m = (MovieList)cmbGenre.SelectedItem; if(!m.CheckOut(l)) MessageBox.Show("This Movie is already out."); } private void MovieSoldDrop(object sender, DragEventArgs e) { object o = e.Data.GetData(DataFormats.Serializable); ListViewItem l = (ListViewItem)o; DialogResult dr = MessageBox.Show("Are you sure you want to sell " + l.Text + "?" , "Sell This Video", MessageBoxButtons.YesNo); if(dr == DialogResult.No) return; //Very important!! If I did not remove this ListViewItem from the source //list I would need to clone this ListViewItem before I add it to the //lstSold control. lstRentals.Items.Remove(l); lstSold.Items.Add((ListViewItem)l); lstSold.Items[lstSold.Items.Count-1].ImageIndex = 0; //Look at the genre combo box to see which movie list to delete this //ListViewItem from. MovieList m = (MovieList)cmbGenre.SelectedItem; m.Remove(l); } #endregion
VB
#Region "Delegates" Private Sub GenreClick(ByVal sender As Object, ByVal e As EventArgs) Dim cmb As ComboBox = CType(sender, ComboBox) GetList(CType(cmb.SelectedItem, MovieList)) End Sub Private Sub MovieRentalStartDrag(ByVal sender As Object, _ ByVal e As ItemDragEventArgs) lstRentals.DoDragDrop(e.Item, DragDropEffects.Move) End Sub Private Sub MovieRentalDragAcross(ByVal sender As Object, _ ByVal e As DragEventArgs) e.Effect = DragDropEffects.Move End Sub Private Sub MovieDragInto(ByVal sender As Object, ByVal e As DragEventArgs) 'The data must come from the lstRentals ListView Dim o As Object = e.Data.GetData(DataFormats.Serializable) If o.GetType() Is GetType(ListViewItem) Then Dim l As ListViewItem = DirectCast(o, ListViewItem) If (l.ListView Is lstRentals) Then e.Effect = DragDropEffects.All Else e.Effect = DragDropEffects.None End If End If End Sub Private Sub RentalCartDrop(ByVal sender As Object, ByVal e As DragEventArgs) Dim o As Object = e.Data.GetData(DataFormats.Serializable) Dim l As ListViewItem = DirectCast(o, ListViewItem) Dim dr As DialogResult = MessageBox.Show("Confirm Rental of " + l.Text, _ "Rent Video", _ MessageBoxButtons.YesNo) If dr = DialogResult.No Then Return End If 'Look at the genre combo box to see which movie list to delete this 'ListViewItem from. Dim m As MovieList = CType(cmbGenre.SelectedItem, MovieList) If Not m.CheckOut(l) Then MessageBox.Show("This Movie is already out.") End If End Sub Private Sub MovieSoldDrop(ByVal sender As Object, ByVal e As DragEventArgs) Dim o As Object = e.Data.GetData(DataFormats.Serializable) Dim l As ListViewItem = DirectCast(o, ListViewItem) Dim dr As DialogResult = MessageBox.Show("Are you sure you want to sell " + _ l.Text + "?", _ "Sell This Video", _ MessageBoxButtons.YesNo) If dr = DialogResult.No Then Return End If 'Very important!! If I did not remove this ListViewItem from the source 'list I would need to clone this ListViewItem before I add it to the 'lstSold control. lstRentals.Items.Remove(l) lstSold.Items.Add(DirectCast(l, ListViewItem)) lstSold.Items(lstSold.Items.Count - 1).ImageIndex = 0 'Look at the genre combo box to see which movie list to delete this 'ListViewItem from. Dim m As MovieList = CType(cmbGenre.SelectedItem, MovieList) m.Remove(l) End Sub #End Region
Most of these delegates take care of the housekeeping during drag-and-drop operations.
The GenreClick method calls GetList. This helper function follows.
C#
private void GetList(MovieList list) { lstRentals.BeginUpdate(); lstRentals.Items.Clear(); lstRentals.Items.AddRange(list.Items); lstRentals.EndUpdate(); }
VB
Private Sub GetList(ByVal list As MovieList) lstRentals.BeginUpdate() lstRentals.Items.Clear() lstRentals.Items.AddRange(list.Items) lstRentals.EndUpdate() End Sub
Several things need to happen for a drag-and-drop operation to work:
The source object must be serializable. Most .NET objects are, but if you want to drag and drop your own object, it must inherit from the ISerializable interface.
You must hook to an event (usually MouseDown) where you use the DoDrag method. This method needs the object being dragged and the DragEffects enumeration you want. Note that the ListView has a special ItemDrag event that I use here to start the process.
You must set up the receiving control to allow drop operations. You do this with the AllowDrop property.
Hook up to the DragEnter event for the destination control. Here you will detect the object being dragged and either allow the operation to continue or deny it.
Hook up to the DragDrop event in the destination control and perform whatever operation is needed with the data from the source object.
You can hook into some other events with drag and drop, but the preceding list covers the basics.
Along the way, you need to know a couple of things. First, you need to know that the DragEffects enumeration allows you to specify if you want the operation to be a copy, move, or cancelled. If you use DragEffects.None, you will effectively cancel the drag operation.
Second, you need to know how to get the data that you are transferring over. There are a few standard data types that the drag-and-drop operation knows about. These types are the most common types of data to be moved. Some of the data types are as follows:
Bitmap
CSV format text
HTML
Text
You should be aware of a whole host of other data types. See the online help for the DataFormats class. If your object does not fall into one of these nice categories, then you will need to use DataFormats.Serializable like I did. This returns an object that you will need to cast to the proper data type before you can use it.
Note that in the MovieSoldDrop event, I remove the ListViewItem object from its source ListView control before I add it to the destination ListView control. I need to do this because common sense dictates that it should only be in one bin at a time. Also, I need to do this because the object can be in only one ListView control at a time. If I want it in both, I need to clone the ListViewItem before I add it to the second control.
The last thing I do in the drag-and-drop operation here is to remove the ListViewItem object from its MovieList collection. Now, as I change genres using the ComboBox, I will not see this movie again when I reload its genre collection.
If you ran the program now, nothing would happen. Although this seems to happen all too often with released code, at least you can say you know something was left out here. You will need to fill in the constructor with initialization code.
Listings 5-3a and 5-3b show the code for the constructor. Note that I am doing a little exception handling in here as far as reading in the movie files go.
![]() |
public Form1() { InitializeComponent(); //Get the movie ListViewItems try { ActionMovies = new MovieList("ActionMovies.txt"); } catch { ActionMovies = null; } try { DramaMovies = new MovieList("DramaMovies.txt"); } catch { DramaMovies = null; } try { ComedyMovies = new MovieList("ComedyMovies.txt"); } catch { ComedyMovies = null; } //Set up the rental ListView lstRentals.View = View.Details; lstRentals.AllowColumnReorder = true; lstRentals.GridLines = true; lstRentals.FullRowSelect = true; lstRentals.AllowDrop = true; lstRentals.ItemDrag += new ItemDragEventHandler (this.MovieRentalStartDrag); lstRentals.DragEnter += new DragEventHandler (this.MovieRentalDragAcross); lstRentals.Columns.Add("Title", -2, HorizontalAlignment.Center); lstRentals.Columns.Add("Release Date", -2, HorizontalAlignment.Center); lstRentals.Columns.Add("Running Time", -2, HorizontalAlignment.Center); lstRentals.Columns.Add("Format", -2, HorizontalAlignment.Center); lstRentals.Columns.Add("In Stock", -2, HorizontalAlignment.Center); //Now set up the For-Sale ListView BigIcons = new ImageList(); BigIcons.Images.Add(Image.FromFile("movie.bmp")); lstSold.LargeImageList = BigIcons; lstSold.View = View.LargeIcon; lstSold.AllowDrop = true; lstSold.DragEnter += new DragEventHandler(MovieDragInto); lstSold.DragDrop += new DragEventHandler(MovieSoldDrop); //Fill the rental box picRental.SizeMode = PictureBoxSizeMode.StretchImage; picRental.Image = Image.FromFile("cart.bmp"); picRental.AllowDrop = true; picRental.DragEnter += new DragEventHandler(MovieDragInto); picRental.DragDrop += new DragEventHandler(this.RentalCartDrop); //Fill the ComboBox. This MUST be done after setting up the //rental listView control if(ActionMovies != null) cmbGenre.Items.Add(ActionMovies); if(DramaMovies != null) cmbGenre.Items.Add(DramaMovies); if(ComedyMovies != null) cmbGenre.Items.Add(ComedyMovies); cmbGenre.SelectedIndexChanged += new EventHandler(this.GenreClick); //Setting the index automatically fires the event cmbGenre.SelectedIndex = 0; }
![]() |
![]() |
Public Sub New() MyBase.New() 'This call is required by the Windows Form Designer. InitializeComponent() 'Get the movie ListViewItems Try ActionMovies = New MovieList("ActionMovies.txt") Catch ActionMovies = Nothing End Try Try DramaMovies = New MovieList("DramaMovies.txt") Catch DramaMovies = Nothing End Try Try ComedyMovies = New MovieList("ComedyMovies.txt") Catch ComedyMovies = Nothing End Try 'Set up the rental ListView lstRentals.View = View.Details lstRentals.AllowColumnReorder = True lstRentals.GridLines = True lstRentals.FullRowSelect = True lstRentals.AllowDrop = True AddHandler lstRentals.ItemDrag, New ItemDragEventHandler _ (AddressOf MovieRentalStartDrag) AddHandler lstRentals.DragEnter, New DragEventHandler _ (AddressOf MovieRentalDragAcross) lstRentals.Columns.Add("Title", -2, HorizontalAlignment.Center) lstRentals.Columns.Add("Release Date", -2, HorizontalAlignment.Center) lstRentals.Columns.Add("Running Time", -2, HorizontalAlignment.Center) lstRentals.Columns.Add("Format", -2, HorizontalAlignment.Center) lstRentals.Columns.Add("In Stock", -2, HorizontalAlignment.Center) 'Now set up the For-Sale ListView BigIcons = New ImageList() BigIcons.Images.Add(Image.FromFile("movie.bmp")) lstSold.LargeImageList = BigIcons lstSold.View = View.LargeIcon lstSold.AllowDrop = True AddHandler lstSold.DragEnter, New DragEventHandler(AddressOf MovieDragInto) AddHandler lstSold.DragDrop, New DragEventHandler(AddressOf MovieSoldDrop) 'Fill the rental box picRental.SizeMode = PictureBoxSizeMode.StretchImage picRental.Image = Image.FromFile("cart.bmp") picRental.AllowDrop = True AddHandler picRental.DragEnter, New DragEventHandler(AddressOf MovieDragInto) AddHandler picRental.DragDrop, New DragEventHandler(AddressOf RentalCartDrop) 'Fill the ComboBox. This MUST be done after setting up the 'rental listView control If Not ActionMovies Is Nothing Then cmbGenre.Items.Add(ActionMovies) If Not DramaMovies Is Nothing Then cmbGenre.Items.Add(DramaMovies) If Not ComedyMovies Is Nothing Then cmbGenre.Items.Add(ComedyMovies) AddHandler cmbGenre.SelectedIndexChanged, New EventHandler _ (AddressOf GenreClick) 'Setting the index automatically fires the event cmbGenre.SelectedIndex = 0 End Sub
![]() |
Let's look at what's going on here. First, I initialize three MovieList objects. If the objects fail to initialize properly, I set these objects to null. By doing this, I allow the program to run correctly with only one or two valid MovieLists. The most likely reason for a MovieList object failing to initialize properly is an inability to read the text file.
After this, I initialize the two ListView controls and I put a picture of a shopping cart in the PictureBox control. Notice that I set up this PictureBox for drag-and-drop operations as well. The last thing I do in this constructor is fill the ComboBox. Here is the code again:
//Fill the ComboBox. This MUST be done after setting up the //rental listView control if(ActionMovies != null) cmbGenre.Items.Add(ActionMovies); if(DramaMovies != null) cmbGenre.Items.Add(DramaMovies); if(ComedyMovies != null) cmbGenre.Items.Add(ComedyMovies); cmbGenre.SelectedIndexChanged += new EventHandler(this.GenreClick); //Setting the index automatically fires the event cmbGenre.SelectedIndex = 0;
If the MovieList object is not null (nothing in VB) I add the object to the ComboBox. Remember how I overrode the ToString() method in the MovieList class? The ComboBox gets the text to display from the ToString method of the object that I add.
Once I set the event handler, I set the SelectedIndex property to the first item. Doing this programmatically automatically calls the delegate for the event handler. This is why this operation is left until last. If I did it before finishing the setup, I would have bugs galore.
Run the program and play with it. Figure 5-8 shows this form as it is running.
This form does everything you would expect it to, with perhaps a few exceptions. You are not able to reverse a checkout and you are not able to put a movie from the For Sale bin back into the Rentals bin. You have enough knowledge to do this yourself if you are so inclined.
Play around with the Rentals ListView. You are able to swap columns. You are not able to sort them, though. Sorting columns is a common thing in this kind of control and your users will expect it.
You can set up sorting by hooking into the ColumnClick event and reversing the current sort order. This allows you to click a column and go from a descending sort to an ascending sort. The problem with this is that it sorts only on the first column. I know that when I come across ListViews like this, I expect to click any column and have the list sorted according to that column. This last piece of code accomplishes this.
Remember the ListViewSorter class I had you make? I bet you forgot all about that. Currently it has no code in it. What follows is the code for this class.
using System; using System.Windows.Forms; using System.Collections; namespace MovieList_c { /// <summary> /// This class sorts a ListView control by column /// </summary> public class ListViewSorter: IComparer { private int mCol; private SortOrder mOrder; public ListViewSorter(int column, SortOrder order) { mCol=column; mOrder = order; } public int Compare(object x, object y) { int returnVal = String.Compare(((ListViewItem)x).SubItems[mCol].Text, ((ListViewItem)y).SubItems[mCol].Text); if(mOrder == SortOrder.Descending) return (returnVal *= -1); else return returnVal; } } }
VB
Option Strict On Imports System.Windows.Forms Imports System.Collections Public Class ListViewSorter Implements IComparer Dim mCol As Integer Dim mOrder As SortOrder Public Sub New(ByVal column As Integer, ByVal order As SortOrder) mCol = column mOrder = order End Sub Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer _ Implements System.Collections.IComparer.Compare Dim returnVal As Integer = String.Compare( _ (CType(x, ListViewItem)).SubItems(mCol).Text, _ (CType(y, ListViewItem).SubItems(mCol).Text)) If mOrder = SortOrder.Descending Then returnVal *= -1 Return returnVal Else Return returnVal End If End Function End Class
This class allows you to override the normal compare routine of the ListView. Add the following delegate to the main form.
C#
private void ColumnSort(object sender, ColumnClickEventArgs e) { ListView lvw = (ListView)sender; ArrayList SortList = (ArrayList)lvw.Tag; SortList[e.Column] = (SortOrder)SortList[e.Column] == SortOrder.Ascending ? SortOrder.Descending : SortOrder.Ascending; lvw.Sorting = (SortOrder)SortList[e.Column]; lvw.BeginUpdate(); lvw.ListViewItemSorter = new ListViewSorter(e.Column, lvw.Sorting); lvw.Sort(); lvw.EndUpdate(); }
Private Sub ColumnSort(ByVal sender As Object, ByVal e As ColumnClickEventArgs) Dim lvw As ListView = CType(sender, ListView) Dim SortList As ArrayList = CType(lvw.Tag, ArrayList) SortList(e.Column) = IIf(DirectCast(SortList(e.Column), SortOrder) = _ SortOrder.Ascending, _ SortOrder.Descending, _ SortOrder.Ascending) lvw.Sorting = DirectCast(SortList(e.Column), SortOrder) lvw.BeginUpdate() lvw.ListViewItemSorter = New ListViewSorter(e.Column, lvw.Sorting) lvw.Sort() lvw.EndUpdate() End Sub
What I have here is an array of sort orders for each column in the ListView. Depending on which column you select, this method gets the last sort order for that column and changes it to the opposite order. It then redirects the ListViewItemSorter property to use the ListViewSorter class, which sorts the ListView based on the column and sort order passed in. Notice that the sort order array was retrieved from the Tag property of the ListView control.
To make this all work, add the following code to the constructor just below where you initialized the lstRentals ListView control.
C#
ArrayList order = new ArrayList(); for(int k=0; k<lstRentals.Columns.Count; k++) order.Insert(k, SortOrder.Ascending); order.TrimToSize(); lstRentals.Tag = order; lstRentals.ColumnClick += new ColumnClickEventHandler(ColumnSort);
VB
'Make something that will hold the current sort order for the current column Dim order As ArrayList = New ArrayList() Dim k As Integer For k = 0 To lstRentals.Columns.Count - 1 order.Insert(k, SortOrder.Ascending) Next order.TrimToSize() lstRentals.Tag = order AddHandler lstRentals.ColumnClick, New ColumnClickEventHandler _ (AddressOf ColumnSort)
Notice that I keep the sort order array as a part of the ListView control. Doing this obviates the need for me to keep any global variables, and it also eliminates the need to keep track of which array belongs to which ListView control. The ColumnSort delegate does not care which ListView control needs sorting, nor does it care how many columns that control has.
Tip |
It is always best to keep all data and functionality concerning an object within that object itself. This basically makes your object self- aware and able to handle its own needs. |
Now you can run the program and click different columns in the lstRentals ListView control. You will be able to sort in ascending or descending order based on the column that you clicked. This functionality is not just a nicety in data entry programs; it is required.
One note here. Click the column header for the running time. See that the sort order is 98, 94, 36, 120 for descending and the reverse for ascending. How can this be? Take a piece of paper and the ASCII chart and compare the text representations of these numbers. You will see that the order is correct (120 begins with a 1 so its sort order is first).
What to do? In order to fix things like this, you will need to make a compare routine for each column. If you have a column of numbers, you will need to sort according to numerical order.
[2]There are those collections again!