I have a list of about 150 students that I would like to organize. I need the user to be able to select individual students, and I was hoping to use a treeview control to separate them into Sophomore, Junior, Senior(No freshmen) so that way it is easier to locate a single member.
My problem with this plan is that checkboxes are beside both the root and the child nodes and I only want the user to be able to select the individual students. So my question is is there a way to only put checkboxes beside the students or is there another control that can achieve the same effect?
In WinForms, the answer is kind of wonky but it's easier than what I first thought.
The StateImageIndex and StateImageList properties can be used to put icons next to each node. You can exploit this by creating an image list where 0=transparent, 1=unchecked, 2=checked checkboxes. Then, you set the StateImageIndex on each node to the appropriate value. It's a pain in the butt compared to just working with a checkbox, but it works.
You can also override the OnDrawNode() method of TreeNode and manually draw nodes with or without checkboxes. Honestly, that's tougher than using the state image list but it's an option. This page has some answers and several links to examples.
I like WPF and not enough people use it, so right now I'm working on a WPF solution that will use real checkboxes. I don't like the WinForms "solution" because it involves having to fiddle with making sure you get decent checkbox graphics. The reason I'm making a second post is most people balk at the thought of not using WinForms, so I want it to show up as a kind of "advanced answer".
First, a fast-track "Why did I think WPF is good for this example?". I'm going to gloss over many WPF concepts but mark them with "If you're curious, ask." The idea is to keep this post small and focus on the application as an example of WPF, not a tutorial.
In Windows Forms, when you want to customize a control, you are limited to using properties of the control or overriding the methods that paint the control. This gives you a great deal of freedom but can be a lot of work. Sometimes you don't have the ability to change how the control draws; in this case you're out of luck. It's also hard to associate data with UI objects in Windows Forms. There is a data binding framework, but it's difficult enough to use most people end up writing code that synchronizes the UI and data themselves.
In WPF, control customization is always an option. You can always apply a new template to a control. WPF also has a very strong data binding framework; it's harder to write a WPF application *without* data binding. WPF's templated controls and more natural data binding will make this project easy to implement.
We start with the Student class. A student has a name, and a rank ("rank" is what I'm using to denote Freshman, Sophomore, etc.) I added an IsSelected property for checkbox selection later; I left a comment in the code to indicate this is generally frowned upon in WPF but keeps the example simple. Notice the INotifyPropertyChanged implementation. WPF's data binding requires that my classes either implement this interface or use a special kind of property called a dependency property; for technical reasons we don't need dependency properties. If you're curious, ask!
Next, StudentCollection. I'm going to need to put many students in something, and for data binding to work with that something I have to have the students in a collection that implements INotifyCollectionChanged(Of T). It happens that ObservableCollection(Of T) does this, so that's one thing down. As a shortcut, I made StudentCollection always fill itself with some default students.
I actually made the decision on StudentCollection based on the control I want to use to display it. A WPF TreeView expects a hierarchical data source; in other words it would want an object that includes 4 collections: one for freshmen, sophomores, etc. I could have done that, but it would have been more complicated. Instead, I'm going to use ListBox because it has grouping and sorting capabilities that do not require a hierarchical data collection. This means to some extent I'll lose the +/- look of the TreeView, but I'll be able to solve this problem when I get there.
So, MainWindow.xaml. ListBox on the left, buttons on the right. The buttons let you add and remove students and also list any that are checked. The XAML for the UI is mostly dedicated to the ListBox modifications, so let's talk about those.
When the XAML started its life, the ListBox line looked like this:
This told it to use the StudentCollection I set up in the window's resources. Right now the ListBox doesn't know how to display students though; it just displays "StudentLister.Student". So, how do I tell a ListBox how to display one of these? The answer is a data template.
The ItemTemplate property takes a special object called a data template. A data template is like a tiny user control that knows how to bind to the properties of some class. When the ListBox has to display something, it first checks to see if it has an item template that matches it. Our item template is simple: a checkbox bound to IsChecked and a TextBlock bound to Name:
This gets us a checkbox and name displayed for each student. You also have actual ListBox selection; I don't like it but I'm going to leave that alone for now. Ask questions about how to pull that off if you're curious.
You wanted the items grouped based on the class level; to do this we need to use a concept called the "collection view". This is a kind of behind-the-scenes wrapper around a collection that tells the ListBox the rules it needs to use for sorting, filtering, and grouping data. All we're interested in is grouping, so that's all I did. You group data by giving the collection view a GroupDescription that describes the property that determines grouping. Then you have to give the ListBox a data template to represent the group headers in its GroupStyle property. I chose a text block with bold text for this. One interesting point: the item the group style binds to is not a Student but a CollectionViewGroup with different properties. So, instead of binding to Student.Rank, I am binding to CollectionViewGroup.Name which will have the value of the group item, which I know to be the student's rank. Here's the XAML changes.
First, I had to add the collection view to the resources and add the group descriptions:
Voila! A grouped listbox that is coincidentally sorted by class rank since the first four items are sorted (making it sort the groups more properly would be a more complicated task; ask if you're curious!)
The buttons are the most boring part. "Add student" generates a student with a new random name and rank and adds them to the collection. "Delete Student" removes the student that is selected (not checked) from the collection. "List checked students" loops over all of the students and prepares a list with the names of each checked student. That stuff is in MainForm.xaml.vb and is pretty basic.
Whew. It was a long explanation but it's a decent demonstration of how WPF takes a different approach. This is missing some stuff that a TreeView implementation would have: there's no collapsing of groups. There's also no real sorting of the groups other than "the order they appear in the collection". It'd be relatively easy to address these, but it would make this already long post longer and harder to follow. Ask if you're curious.
Or, demand a sample of the WinForms solution; someone might chip in!
If you want to owner-draw TreeView nodes in WinForms, you are given two options. OwnerDrawText allows you to draw text yourself, OwnerDrawAll requires you to draw everything, including plus/minus signs and lines. I'm not sure which would work for your needs, but the latter sounds like a lot of work.
As far as drawing decent check boxes, the ControlPaint class will probably do a great job there. It should draw check boxes exactly as they would appear in any CheckBox control in your application.
Thank you for your time in posting this. I have been searching the internet for a few hours and your answer is by far most thorough and applicable one. That being said, I should probably let you know that I am a new programmer. I am in high school, and I started learning vb.net only this summer. So, your answer is a lot to digest. Nevertheless, I understood a lot of what you said. I am a little confused by the XAML parts, mainly because I don’t know XAML. So, from the point of view of a beginner, can you explain generally what you have done? I mean, did you make a new control or change an existing one in code? Also can you explain briefly, what XAML is and it’s relation to visual basic. I don’t need an full history of XAML or anything, but I am just wondering whether it is something I should look into learning the basics of or if I should try to master vb.net first.
Also, I need help adding the Student lister folder correctly. I have added the entire folder to the projects folder for visual studio, and I doubleclicked the studentlister.vbproj file. What opened seems to be what you made, but there are various errors:
1. Assembly 'StudentLister' was not found. Verify that you are not missing an assembly reference. Also, verify that your project and all referenced assemblies have been built.
2. The type 'local:StudentCollection' was not found. Verify that you are not missing an assembly reference and that all referenced assemblies have been built.
3. 'Sub Main' was not found in 'StudentLister'.
4. Name 'FindResource' is not declared.
5. 'EditorBrowsableAttribute' is ambiguous in the namespace 'System.ComponentModel'.
6. 'GeneratedCodeAttribute' is ambiguous in the namespace 'System.CodeDom.Compiler'.
7. 'ApplicationSettingsBase' is ambiguous in the namespace 'System.Configuration'.
8. 'HelpKeywordAttribute' is ambiguous in the namespace 'System.ComponentModel.Design'.
9. 'INotifyPropertyChanged' is ambiguous in the namespace 'System.ComponentModel'.
10. 'PropertyChangedEventArgs' is ambiguous in the namespace 'System.ComponentModel'.
Many of these were in multiple places, but I only listed them once. Do you know what these mean and how to fix them? Thank you again for your effort and patience.
I do not have the problems that you are having. I double-checked by opening the project at home in VB 2010 Express, and it builds and functions as intended. The only things I can figure went wrong are one of the following:
You have VB .NET 2008
You opened the XAML file before building the project
You should have clicked on the .sln file instead of the .vbproj file
The first problem would just mean you were missing some references; it'd be easier for me to create a VS 2008 version of the project than figure out which ones you need to add.
The second one is what I think is happening. WPF/XAML are pretty finicky and "you haven't built" yet tends to result in a ton of errors. I'm curious about the "ambiguous in namespace..." errors; it'd indicate that .NET thinks it sees two different types with the same name and that shouldn't be happening; my guess is the XAML parser just went into the weeds. "Assembly 'StudentLister' was not found" means that the EXE this project builds isn't there; the XAML parser needs it to instantiate types like StudentCollection. As unintuitive as this seems, the right way to fix these compile errors is likely to compile again. (Don't get me started on spurious XAML errors...)
The third is probably not the problem, but worth knowing. Individual .vbproj files represent projects, but it's typical for large applications to have multiple projects. .sln solution files can contain multiple projects and you should usually open a project via its solution file.
Have you looked at the examples for Windows Forms I liked to and read snarfblam's post? It might be a bit easier for you to get a working implementation off of those.
I'm not ignoring your "what the heck did the WPF do?" question; it's large enough I want to do it in a separate post so I could get these answers to you more quickly.
First, one you didn't ask: What is WPF?
WPF is a different rendering architecture for applications. Windows Forms has been the architecture since the beginning; it is based on GDI+ which is based on GDI, which has been the only architecture since Windows 3.1. Why'd they make a new one? A lot has changed in 30 years. GDI was optimized for drawing static, non-overlapping, opaque rectangles. Win2K introduced overlapping, transparent stuff to the mix, but this wasn't fully carried through to Windows Forms in .NET. Animation is not built-in in Windows Forms, so something as simple as "move this button from A to B" involves timers, interpolation code, and a lot of work to make it smooth. In the past 5 or 10 years, a fruity computer company has been kicking Microsoft's butt on the user experience side with a UI framework that makes heavy use of transparency and animations. WPF is a kind of answer to this problem. Transparency support is built-in, and animations are easy. It also has very strong data binding capabilities. Most interesting to me (I like writing custom controls) is the notion of templateable controls: every control's UI is dictated not by program code for drawing its elements but by a "control template" that dictates the elements and how they get their data. How does this work? XAML is a big help.
Now, what is XAML?
XAML stands for something but I really don't understand why everyone starts with "XAML stands for...". No one calls it by its full name. Just call it "Za-Mul", rhymes with "camel". XAML is an XML-like markup language for describing WPF UI, but it also has extensions for declaring data bindings and event handlers. Some people don't like it, and I have my issues with it, but it is a *really* easy way to whip up a UI with a fluid layout compared to Windows Forms. Consider an application like Windows Explorer, where you have a TreeView on the left and some kind of filesystem control on the right. Here's the code to create something like it in Windows Forms (using imaginary API; some of it I don't know by memory):
Dim panel As New TableLayoutPanel()
panel.Dock = DockStyle.Fill
Dim tv As New FileSystemTreeView()
tv.Dock = DockStyle.Fill
Dim list As FileSystemListView()
list.Dock = DockStyle.Fill
It works, but you have to read the whole thing to get a picture of what it looks like. Here's equivalent XAML:
I find that to be much more descriptive and easier to write/understand, but others feel differently. (Incidentally, when the project is built the XAML parser turns the XAML into code that looks similar to the Windows Forms code; you don't *have* to use XAML but it makes many things easier.)
Next, what did I do?
I did not make a new control, though it's easiest to visualize what I did in terms of controls.
In Windows Forms, to modify the checkboxes of a control that doesn't automatically include them, you have to draw the checkboxes yourself. For example, in a TreeView you can override the OnDrawNode() method and add checkboxes if you feel like it; the code would look sort of like this (again, imaginary API):
Overrides Sub OnDrawNode(ByVal e As DrawNodeEventArgs)
Dim height As Integer = e.Bounds.Height
Dim centerHeight As Integer = height \ 2
Dim checkboxHeight As Integer = GetSystemCheckboxHeight()
Dim checkboxPosition As New Point(5, centerHeight - (checkboxHeight \ 2))
New Rectangle(checkboxPosition, New Size(checkboxHeight,
Dim textPosition As New Point(5 + checkBoxPosition + 5, 0)
Dim textBounds as New Rectangle(textPosition.X, textPosition.Y, e.Bounds.Width - textPosition.X, e.Bounds.Height)
e.Graphics.DrawString(e.Node.Text, Me.Font, Brushes.Black, textBounds)
It's easy to a veteran: calculate where the checkbox should be, draw it, then calculate where the text should be and draw it. You can modify the above to include/exclude checkboxes as needed, and you'd have to add stuff to allow for checked/unchecked boxes, but the idea is simple.
In WPF, you do not change how controls draw in the same manner. If you don't like the way a control looks, you change its "template". The template is the XAML that dictates what the control looks like. For example, a simplification of ListBox's control template looks something like this:
If I wanted a ListBox that stacked items horizontally instead of vertically, I'd create a template for the ListBox that sets the Orientation property of the StackPanel to "Horizontal". Done. That's not what I did though. See the ItemsPresenter? Controls that contain multiple data items use that to represent the part of their template where the items will be displayed. When the ItemsPresenter displays itself, it uses an algorithm that looks like this:
For each item in the control:
If a DataTemplate for this item's type is available:
Create UI elements from the DataTemplate
Bind the UI elements to this item
Add the UI elements to the display
Create a TextBlock
Store thisItem.ToString() in the TextBlock
Add the TextBlock to the display
Data templates are key. The "items controls" like ListBox use them to determine what will be used to display their data. My item template had a checkbox and a TextBlock, so for each student the ListBox creates those two elements and adds them to its display.
In terms of Windows Forms, it's as if I created a custom UserControl to display Student objects. Then, I'd have to create a custom control that displayed a list of Student objects. For each student, the control would create a new instance of my UserControl and add that control to the display. Alternatively, it's roughly equivalent to if I used owner-draw in the ListBox and manually drew check boxes and text. In my opinion, it's much easier.
I can't really explain WPF in one thread, nor could I explain Windows Forms in one thread. These are very large topics that warrant separate study. If you're interested in WPF, I suggest starting with Josh Smith's guided tour of WPF. I don't know any particular tutorial paths for Windows Forms, but since it's been around for more than 8 years there's tons of tutorials online.
Don't mistake my effort on this subject as a suggestion that you *shouldn't* use Windows Forms. This is merely an instance where I feel WPF is a more natural fit, and there is very little WPF information on this forum. I'm trying to change that
I do have vb.net 2008 by the way, so is probably a part of the problem with the StudentLister. Nevertheless, I found a way late last night to get the checkboxes off where someone had posted code for the same thing I wanted, I think they used they state image thing you mentioned earlier. I still not exactly sure what they did either, but I'll probably just use it and see if I can figure out what they did as I become more experienced with vb.net. So, you probably don't need to hassle with making a 2008 version unless you feel so inclined. Thanks for your explanation though, and I think I may look into at least learning some more about WPF--it sound like it is more flexible and, once you know it, a little easier for customization