In this second part of a four-part series on learning how to use the WPF through an example, we continue constructing our to-do list application. You'll learn how to adjust the sizes of the "add task" and "delete task" buttons, and how to use a template to neaten the layout of the list.
Now that the Grid is prepared, it's time to go ahead and add interactive controls to the mix. Remember, we're going to need a list of some sort to contain the tasks (after all, this is a to-do list), along with two buttons, one that will add a new task to the list, and another that will delete an existing task from the list. For the list, we're going to use a ListBox control, and for the buttons, we're going to use two Button controls.
Adding the basic controls to the application's XAML isn't very difficult. They simply need to be placed within the Grid tag:
Each control is given a descriptive name and a position within the grid. We must specify both the row and the column, and with the ListBox, we also specify the ColumnSpan since it has to span across both of the grid's columns. Additionally, the two buttons are given some text using the Content attribute.
If you run the application, you should see the controls positioned properly:
However, the two buttons are jammed right up against each other and against the task list. It would be nice if there were some space in between. This can be accomplished using the Margin property that I mentioned earlier. The Margin property can be set using either an attribute or an element, and we can either set a uniform thickness or specify the size of all the margins individually.
Let's first take a look at setting a uniform thickness using an attribute. This only involves adding a Margin attribute to the appropriate control and setting it to a proper thickness (in device-independent pixels, by default):
This would create a margin of five pixels around all of the sides of AddButton. This makes the application look a bit odd, though. Margins only in between the buttons and on top of the buttons will do fine. So, let's set the margin of each side individually. To do this, we need to specify four measurements, separated by commas. The first measurement is the left margin, the second is the top margin, and so forth, in a clockwise motion.
Now AddButton looks right, and we only need to fix DeleteButton. For DeleteButton, let's specify the margin using an element (although you may want to go back and change this in order to maintain consistency). To do this, an element named object.Margin (where “object” is replaced with the name of the actual tag) is used inside of the object's tag. Then, inside of this element, an element named Thickness is created which contains the actual values:
Notice that this method is fairly readable, with the measurements set in appropriately-named attributes (Left and Top here, but Right and Bottom also exist).
The controls are positioned correctly, and now we can turn our attention to the tasks in the to-do list. Obviously, the tasks need to be drawn from some data source. Although there are several options available, the easiest way to store and retrieve the data is through a simple XML file. Preparing this file won't require a lot of work, and the result will be readable and manageable.
Each task needs to have a name, a description, a priority, and a status (done or not done). Describing this in XML is very easy. Each task can be represented by a Task element, and inside of each Task element can be elements to describe the properties just listed. So, the XML to describe the task of watering flowers might look something like this:
<Task>
<Name>Water Flowers</Name>
<Priority>Medium</Priority>
<Done>No</Done>
<Description>
The flowers need to be watered, or else they'll die.
</Description>
</Task>
As you can see, we haven't opted for anything complex. We're only making a simple to-do list.
Go ahead and add a new XML file to the project. Name it Tasks.xml and fill it with some tasks:
<?xmlversion="1.0"encoding="utf-8" ?>
<Tasks>
<Task>
<Name>Water Flowers</Name>
<Priority>Medium</Priority>
<Done>No</Done>
<Description>The flowers need to be watered, or else they'll die.</Description>
</Task>
<Task>
<Name>Eat Breakfast</Name>
<Priority>High</Priority>
<Done>Yes</Done>
<Description>It's the most important meal of the day.</Description>
</Task>
<Task>
<Name>Buy More Ink</Name>
<Priority>Low</Priority>
<Done>No</Done>
<Description>It'll be out eventually.</Description>
</Task>
</Tasks>
By default, the XML file is a resource. This means that the file will be included in the application's resulting assembly. This isn't what we want. In order for the application to function properly, the XML file needs to exist alongside the assembly. That way, it can be modified. To fix this, select the file, and in the Properties Window, change Build Action to Content and Copy to Output Directory to Copy if newer:
Next, the XML file needs to be wired into the ListBox. The first step is to make the data in the XML file accessible to the application by creating an XmlDataProvider. This can be done in the application's XAML, so no code is required. The XmlDataProvider is an example of a resource, which we can define once and use in multiple places. Resource definitions go in the appropriate Resourcessection. The XML file only needs to be used inside of Window1, and so we need to first create an object.Resources element as a child of Window1's Window element:
<Window.Resources>
</Window.Resources>
Then, the XmlDataProvider needs to be created within this section:
There are three attributes that we define. The first is x:Key, which provides the resource with a unique identifier. The second is Source, which, of course, points to the source XML file. The third is XPath, which is set to the query used to generate the collection of elements. Here, we need access to the Task elements, which are located inside of the Tasks root element.
The next step is to hook the ListBox up to the XmlDataProvider. This is really simple to do. Replace the existing ListBox declaration with this:
If you run the application right now, you should see something like this:
As you can see, the ListBox is properly connected to the XML data in Tasks.xml, with one ListBox item for each task, but each task's data are crammed together. Obviously, each task needs to be presented more neatly. It would be nice if each item in the ListBox could have a checkbox to the left, indicating whether or not the task is done, and, on the right, have the title, colored according to the priority (green for low priority, orange for medium priority, and red for high priority), and the description. When the user checks the checkbox, the task is marked as done.
To do this, we need to use templates. Templates are defined in the Resources section. In particular, we need to define a DataTemplate. Inside it, we can lay out controls just as we did in the main window. Let's go ahead and define the DataTemplate:
<DataTemplate x:Key="TaskTemplate">
</DataTemplate>
Above, we create an empty DataTemplate. Inside of it, we can create a Grid to lay out the controls, just as we did with the window:
<Grid>
</Grid>
Inside the Grid, we need to define rows and columns. For the layout described above, we need two columns and two rows. The checkbox will occupy the first column and will span two rows. The title will occupy the second column of the first row, and the description will occupy the second column of the second row. Here are the appropriate definitions:
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="15" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Next, we need to add the controls to the Grid. For the checkbox, we'll use a CheckBox control (of course), and for the title and the description, we'll use two TextBlock controls:
The final step is databinding. We need to match the value of each control with an element in the XML. For the two TextBlock controls, this is very easy:
The CheckBox will require more work, however, as will the colorization according to priority. With the title and the description, we're simply taking string values form the source and using them as string values in the application. With the status and the priority, though, we need to convert a string value into a check and another string value into a color. This will require an extra step and some actual code.