Introduction
I am a very big newbie at CodeProject article submission. I do not understand it to be honest, and it took me hours to even get the horrible formatting that I did. I didn't see any WPF formatting, and my C# formatting came out like crap. If anyone knows any good articles on article submission, or a good article tool (perhaps an offline WinForm or something), I would greatly appreciate it.
Let me start by declaring that I am in no means an expert with WPF (yet). I am, however, an avid user of WPF, with a deep passion and desire to expand my knowledge. I just recently (a couple of weeks ago) picked up shop from the world of WinForms and moved over to WPF. For someone with no markup language experience (no XML, no HTML, etc...), the transfer from WinForms to WPF has been anything but easy. There are still many concepts I haven't attempted to grasp. I have, however, spent countless hours per day for the past couple of weeks surrounding myself in articles and tutorials with WPF. I have had the pleasure to (and not so pleasing) experience of viewing some great articles by users such as Bea the WPF Binding Queen, Josh Smith WPF Guru, and Sacha Barber. I've been following Josh and Sacha (early adopters of WPF on the CodeProject community) for a while, so I've enjoyed their articles without a doubt.
With that out of the way, the reason why I wrote this article is because I simply had to do a lot of digging around to understand some basic concepts of WPF that a lot of blogs out there fail to mention. Even MSDN made it difficult to follow along with their samples, as they "assumed" you understood the underlying concepts.
I'd like this page (along with Josh's: Great WPF Tutorial) to be one of the articles that mention the basic concepts for WPF that should be enough to get developers knee deep in their new found love [ WPF :) ].
Background
(Taking this from Josh Smith's article here (Great WPF Tutorial) since it contains the necessities for WPF development. I myself am using Visual C# 2008 Express w/ Vista.)
"In order to run a WPF application, you need the .NET Framework version 3.0, or greater. Windows Vista has the .NET Framework v3.0 installed by default, so you only need to install it if you have Windows XP SP2" - Josh Smith.
Simple XAML Layout
Let's begin!
I am going to start from the very beginning... Simply skip ahead to a topic that you think you may be interested in if the start is too simple for you.
I've created a new WPF Application called "JumpStartWPF". The IDE looks like this after creation (I've done nothing to it up to this point).
Take a look at the default XAML code here:
<Window x:Class="JumpStartWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="300">
<Grid>
</Grid>
Breaking this down line by line:
It starts off declaring a new Window
and setting some properties. In this case, it's creating references to some default Microsoft namespaces (URLs), setting the Title
text of the Window
, and the Height
and Width
. Notice the first reference it makes though:
x:Class="JumpStartWPF.Window1"
This markup is created for you by default, and points to the namespace/class of your main XAML Window
and code-behind file. In this case, JumpStartWPF
is the namespace we created, and Window1
is the default class name for WPF applications. If you were to change the namespace or class name in this markup, then our main XAML file (Window1.xaml) would not be able to find its code-behind file (Window1.xaml.cs) and would throw an error in the constructor of the code-behind file.
You may be wondering what code-behind means. In the WPF world, code-behind is used religiously as the physical code file behind each XAML object. In this case, our main Window
's XAML file is Window1.xaml. Every XAML file needs a code-behind file to function, so ours is simply named Window1.xaml, and appended .cs (because I'm working in C#) to the code-behind file.
Next, you'll see the opening/closing tags for a Grid
. It is basically a container for other controls (including other Panel
s), just like in the WinForms world, where GroupBox
/DockPanel
/etc. are commonly used as Container
s.
There are many different ways to layout your UI with XAML. You may spend days or weeks deciding what you want your base Panel
(Container
) to be for an application. I myself have settled upon the Grid
. The reason is I feel I have the most control over the Grid
is, with Grid
, you can define rows/columns for your entire window and then place other panels within those newly defined rows/columns appropriately.
Let's do a simple, yet effective example.
<Grid ShowGridLines="True">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
</Grid>
OK, let's step through this.
First, I set the property "ShowGridLines
" of the grid to "True
". This is, in my opinion, really only useful for debugging/layout purposes... all it does is draw dotted lines at each column and row separation of the said [Grid
] panel.
Next, I started defining some simple layout of the window. As you can already see, a lot of WPF concepts are very easy to understand (and a lot aren't, don't get me wrong), but the syntax is what throws some people off. In this case, you can clearly see that I am defining with two new Column
s in my parent [Grid
] panel. This starts out by declaring the Grid
.ColumnDefinitions
(and closing it), then, in between the ColumnDefinition
s. I can start declaring all the columns I want my window to have. You'll see shortly how these come into play.
If you run this application now, you'll see a window with a dotted line down the middle. Pretty cool, eh? Yeah, I know... not really :) This is only the start though!
More Complex XAML Layout
Let's take what we've learned with defining columns, and expand upon that to create a look that looks like an American football field (thanks to ShowGridLines
="True
"). For time/simplicity's sake, I'm not going to draw each yard marker for rows. I'm simply going to draw the end zone rows, and the 50 yard line row. You can easily add more rows for getting the yard markers after you run through this quick example.
Now you might be able to guess that we're going to need to use a new concept in the Grid
layout, which is Row
s.
Now, if you know what an American football field looks like, you'd know that there's a horizontal line splitting the field in half (the 50 yard line). Well, I've never seen one with a vertical line which is what we have now, so let's adapt our current layout to show a horizontal line instead!
Just so you don't get lost, I've done one thing before moving on. I've set the window height to 500 and left the width at 300. American football fields are rectangular and not square, so this gives us a more desirable look.
Now, on with the layout.
So, you can literally replace the word "Column
" with "Row
" in the previous definitions and get the desired effect :)
<Grid ShowGridLines="True">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
</Grid>
Run this now, and you'll see what we want! A horizontal line splitting the field in half.
Now, let's mess around with this some more and add some end zone horizontal lines.
You may be asking yourself, how do we tell the rows how much space to take up? The end zone lines need to only take a certain parentage of space. If we add another row like we have been doing, it will split up each row evenly spaced apart based on the window's height. Well, lucky for us, each RowDefinition
has a Height
property we can set.
There are a few ways to set the height of each row. You can set each row's height by pixel values, percentage values, or auto-fill values.
Pixel values are self explanatory, you can do something like:
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="50" />
<RowDefinition Height="50" />
</Grid.RowDefinitions>
Integral values written like this for width (Column
) or height (Row
) are parsed into Pixel format. The numbers used in this case are giving each row that amount of space. We've just declared 3 rows, and given them at least 50 pixels of space between them.
Now, you could play with this for a few minutes and probably figure out the precise amount of pixels to give each row to get two end zones... but we have another card up our sleeves.
I mentioned before that we could set the percentage values for each row or column. Well, I don't know about you, but I'd much rather define my row's height using percentages than hard coded pixel values. The markup for declaring a percentage value can be written in two ways. Just like in the numbers you're used to, 50% is written as either "50%", or "0.5". The same applies here, only you can replace the "%" symbol with "*".
Back to our example, let's apply this to our American football field.
Currently, the following gives us two rows, each given the same amount of space (represents a 50 yard line).
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
Now, let's make end zones, giving each row a percentage. You can already guess we're going to need at least three rows for end zones. Each end zone itself is a row (2), and we need another row for the remaining space in the field (all 100 yards).
There are two ways to do this:
The old way (3 rows, each given equal space):
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
The new way (3 rows, also each given equal space):
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Notice how in this case, we set the height of each row to "*". This means we want the Row
to automatically be given a Height
to split it evenly based on other rows/window height. This is the same as putting no height at all, but it leads us to our next requirement, which is giving a percentage value.
If you think about this, we could bypass some hacky values for the end zone rows (1st/3rd rows), and just tell the center row (100 yard part of the field) how much space to take up.
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
Now, you should start to see the true power of defining the layout of your window. We've just told the center row to take up 3 times more space than the rest of our rows, based on the window's height.
We've just told our Grid
to create three rows, with the center one taking up 3 times more space than the rest, based on the height of the window.
If you run the application now, you'll see we have a center field and two end zones. But, where's our 50 yard line? Well, we'll need to split our center field into two rows to get that desired effect.
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
So here, I told my Grid
it has 4 rows. The center two rows are to take up 3 times each more than the outside two rows. This basically splits our field in half, and then gives the remaining space to the two outside rows (since they have no width set).
Now that you have the hang of it, let's quickly add some columns to set some sidelines in place.
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
I simply told my Grid
to create three columns, giving the center column 10 times more space than the outside columns.
And, there you have it! An American football field.
Now that you have a Grid
with rows and columns, how do you use it? The Grid
panel offers the ability for its children objects to set which row/column of the grid they appear in. By default, all children of a grid appear in row 0 column 0, but we can set this.
The following examples don't apply to the American football theme. We are simply using our pre-existing layout to add some new controls. Our current Grid
panel, in-fact, has more rows and columns than most basic applications, so it will be a good learning experience.
What's an application without a menu bar? Let's add one!
The markup for a menu system is very easy:
<Menu>
</Menu>
If you run this, you may notice that it's nearly impossible to see. Well, the key word in menu system is "system" :) We haven't given any children to this Menu
object. That, as well, is just as easy as declaring the Menu
itself.
<Menu>
<MenuItem>
<MenuItem />
</MenuItem>
</Menu>
If you run this, you may notice that everything is being bunched up in the top left corner. Remember how I said, by default, all children of a Grid
panel container are set in the 0th row and 0th column? We are seeing that in action now. And lucky for us, each child of the Grid
has the option of explicitly setting where in the grid they want to appear. In our case, a Menu
is typically spanned across the top of a Window
. But this encompasses multiple columns... how do we achieve that?
Again, lucky for us, there is another property that children of a Grid
panel can set, which tells the Grid
that this child is going to span across 'x' amount of columns (or rows).
Let's see this in action.
First, I removed the ShowGridLines
property of the parent Grid
panel because it clunks up the UI :)
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem>
<MenuItem />
</MenuItem>
</Menu>
So, you can see that I set two properties of the Menu
object here. First, I told the menu to hug the top of whatever the parent container is. In this case, our parent is a Grid
, and our default row is "0", so lucky for us, this just happens to be the top of the Window
as well.
Next, I told the Menu
how many columns to span across. We have three columns in our football field, so I simply set the Grid.ColumnSpan
property to "3".
Let's add some text to these menu objects. We can use the Header
content property for this.
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
Run this now, and you'll see the start of a real application! Soon, we're going to make that Exit button do something when Click
ed, but in the mean time, let's add some other controls to our application and put to use all those columns and rows.
First, let's add some color to the border columns (left/right column) of our window. There are many controls that you can use for this... I'm going to use the Border
control though.
So the basic concept is to place a Border
in column 0, spanning all four rows of our field, and to replicate it over on column 3, spanning down that side of the field.
<Border Grid.Column="0" Grid.RowSpan="4" />
<Border Grid.Column="3" Grid.RowSpan="4" />
The Border
control is very powerful, and the capabilities of it are well beyond the scope of this article. Just know that I am purely using it so I can place a control where I need to add some color, and the Border
control stood out in my mind.
So, just like most of our other controls, we can interact with the Border
by setting its properties. You can probably guess that it has a Background
property. So, let's set it.
<Border Grid.Column="0" Grid.RowSpan="4" Background="DarkBlue" />
<Border Grid.Column="3" Grid.RowSpan="4" Background="DarkBlue" />
Notice that the top of the Border
s are overlapping the Menu
? If you have any graphics background, you will know that objects rendered last are drawn on top. In the case of our UI, we've told the Border
s to be drawn after we declared our Menu
.
To fix that, we just need to render the Menu
after the Border
.
<Border Grid.Column="0" Grid.RowSpan="4" Background="DarkBlue" />
<Border Grid.Column="3" Grid.RowSpan="4" Background="DarkBlue" />
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
Cool! Useless, but cool! :) Again, this article is for grasping the concepts of WPF by learning the syntax and some practical uses of its objects.
So anyways, let's add some interaction controls to this UI.
Just so you know where we're going... I eventually want this UI to have the Exit button working. Also, I want to give the user the ability to select/type in a color and the Border
backgrounds change to that color.
Let's first add the controls that we'll need to give the user the ability to select a color. There are a few ways we can do this. We could present a ListBox
/ComboBox
full of preset colors, or we could give the user the ability to type in a color. The typing of the color is a little too complicated for this article.
So, let's begin by adding a Label
to tell our user what this ComboBox
will do, then let's add the ComboBox
.
<Label Content="Choose a color" Grid.Row="2"
Grid.Column="1" VerticalAlignment="Center"
HorizontalAlignment="Center" Margin="0,-50,0,0"/>
Let's examine this ugly piece of hardcoded code.
First, I declared a Label
. I gave it the Content
(Text
) of "Choose a color". Then, the hacktastic-ness begins. First, I wanted it to appear somewhat in the middle of the UI, so I set it to the third row (second in a 0 based index), and the second column. By default, it's placed in the top left of the cell, so I then set the VerticalAlignment
and HorizontalAlignment
to Center
. I then decided that I actually want my ComboBox
directly in the middle of that cell, so I should move this Label
up a little so it would be above my ComboBox
of colors. To do that, I introduced a new property that nearly every control object has, which is called Margin
.
There are a few ways to work with Margin
. There's actually three :)
A Margin
represents the amount of space in between the current control and its parent container's sides. So in my example, I put the Label
in the center of the Cell
, then I told it that I want its Margin
to be moved 0 to the left side, negative 50 on the top, 0 on the right, and 0 on the bottom.
As you probably guessed, it's left, top, right, bottom when you use all four values in the Margin
value. I wanted my Label
to appear above the ComboBox
, so I moved it negative 50 pixels on the Top
coordinate (I could have alternatively done "0,0,0,50").
The two ways Margin
can be used is by only setting one value. I.e., if I did:
Margin="50"
then, it would apply a Margin
of 50 pixels on all four sides of the control.
The last way to use Margin
is to only set two of the values.
Margin="0,50,0,0" -->
Margin="50,0" -->
Margin="0,50" -->
Margin="50" -->
So now, let's add our ComboBox
.
<ComboBox Grid.Row="2" Grid.Column="1"
Width="150" Height="25" SelectedIndex="0">
<ComboBoxItem Content="DarkRed" />
<ComboBoxItem Content="DarkGreen" />
<ComboBoxItem Content="DarkBlue" />
</ComboBox>
So first, we set our ComboBox
to be in the same cell as our Label
. We gave it a Width
of 150 and Height
of 25 (if we don't do this, then the ComboBox
by default will encompass all the space in the cell). We also set the SelectedIndex
to 0, so we are always showing something to the user rather than forcing them to click on it to see something. Also, this will play a part when we need to bind our Border
s to the selected color in the ComboBox
. Then, we gave it children. Just like our main Menu
object, we can give children to objects of type ItemsControl
, which ComboBox
and ListBox
are children of.
Now, take a look at that cell we've been adding our controls to. The XAML we've written for positioning them is rather ugly in my opinion. This is where I like to get creative. Up until now, you've only seen and used the Grid
panel container. Well, I personally only use the Grid
for the initial layout of the window. I don't like placing controls like we've been doing so far in a Grid
. I prefer to add child Panel
s to the Grid
and then add children to them.
We're now going to talk about my favorite panel, which is a StackPanel
. The StackPanel
is extremely simplistic, but very powerful and useful.
Consider the game Solitaire. Visually, it screams out layout (I only said that for the rhyme). Consider what we've learned so far. If you wanted to place collections of cards with a Grid
in the layout they have, you'd have to either be crazy, or insane.
You'd have to have hard-coded margins to separate each card collection and a bunch of other time wasting tasks.
Insert, the StackPanel
. When I look at that Solitaire picture, I see a Grid
containing three rows (the third row is for the victory animation at the end) and two columns. The deck stack is in Row[0]Column[0]. The suit stack is in Row[0]Column[1]. The row stack is in Row[1] and spans across both columns. The victory animation is in Row[2].
So, how is the StackPanel
used in Solitaire? Well, each of those stacks that I mentioned before would be a StackPanel
. A StackPanel
is literally a panel that stacks its children on top of each other (or next to each other depending on how you want it displayed).
Opposed to the grid, the elements within a StackPanel
are tightly knit. In a Grid
, all child elements overlap with each other, which is why we had to set margins to get it to appear somewhat friendly.
Let's put a StackPanel
in the cell that our Label
and ComboBox
currently reside in; then, let's move the Label
and ComboBox
inside the StackPanel
.
<StackPanel Grid.Row="2" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
<Label Content="Choose a color" />
<ComboBox SelectedIndex="0">
<ComboBoxItem Content="DarkRed" />
<ComboBoxItem Content="DarkGreen" />
<ComboBoxItem Content="DarkBlue" />
</ComboBox>
</StackPanel>
Wow! That's much cleaner than setting all the margins and alignments on each control. Each element within the StackPanel
inherits the Grid.Row
, Grid.Column
, and Vertical/Horizontal Alignment properties. So, we can immediately remove and let them implicitly be set. Something that you still may want to do is put a Margin
on the top or bottom of each element to give it a little spacing, but I'll leave that off for this example.
Now, I'm going to move these up one row in the grid to make it more towards the top of the window.
<StackPanel Grid.Row="1" Grid.Column="1"
VerticalAlignment="Center" HorizontalAlignment="Center">
Look how easy that was! We just moved our entire Panel collection up one cell just by changing one property. Before, when we were piggy backing the Grid
panel, we would've had to change it for each control we wanted to move. Imagine if we had an entire UI flow of buttons and comboboxes, and how annoying it would be to manage if it was in a Grid
.
The last topic I want to cover for the layout portion of this article is the Orientation
property of a the StackPanel
. By default, it is set to Vertical
. This simply means that all elements within the StackPanel
are stacked on top of one another. If we set the Orientation
to Horizontal
, then each element would stack side by side. A common usage for this would be something like displaying a user information form. Where you need to gather the Name/Email/City/State of a user, you would typically have a vertical StackPanel
of horizontal StackPanel
s. Each horizontal StackPanel
would be a Label
such as "Name: " and a TextBox
to the right of it where the user could enter this information. And, the vertical StackPanel
would be containing all of those.
Simple XAML to XAML Element Binding
Moving on to a completely different topic, which is my favorite, Databinding. This, to me, is the heart and soul of what drives WPF. In WinForms, the only real binding that took place for me was binding collection controls to backend datasources etc. Well, in WPF, literally everything from the text on a content control, to the background of a panel, to the entire menu system, to the data in each collection, can be bound. So, what is binding? I'll give you a real world example.
Referring back to our current application (which will remain nameless due to its obscurity of meaning), we know that we want the Border
s of the Window
to change color to whatever is currently selected in the ComboBox
. To you WinForms programmers, you would probably register to listen to the ComboBox
's SelectedIndexChanged
event, and would find out the text property of the SelectedIndex
and cast it to a Color
, and change the Border
's Color
to that color. Well, we can actually do this in WPF without ever touching any C#, and we're going to do it through binding.
So, here I'm going to introduce the concept of Element Binding. Again, there are many kinds of binding. We could do it all in C#, we could do part of it in XAML and the other part in C#, but I am going to do it all in XAML. The basic idea is we want the Background
of the Border
to spy on the SelectedItem.Content
property of our ComboBox
. Whatever the ComboBox
says, we want the Border
's background to be that color.
First, in order for any element to be referenced, we need to give it a name. I use the x:Name
property of an element to give it a name.
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0" IsSynchronizedWithCurrentItem="True">
Here, I gave our ComboBox
a name of ComboBoxColors
. This is so other elements can reference this ComboBox
by a unique name. You can name it whatever you want... This is the same as setting the Name
property in C#.
You'll also notice that I set a property called IsSynchronizedWithCurrentItem
to "True". This is to ensure that our ComboBox
is always in sync with whatever item is currently displayed visually. Everything I see online has this set to True... I'm assuming it's defaulted to False, which doesn't make much sense, but anyways...
Now, we are going to do our first binding markup!
Go up to the Border
elements and remove the Background
property completely.
<Border Grid.Column="0" Grid.RowSpan="4" />
<Border Grid.Column="3" Grid.RowSpan="4" />
Here's the basic layout for Element Binding. We need to declare the markup for a Binding
, which is always surrounded by {}
s (curly brackets). Within it, we need to tell what we are binding. There are a multitude of bindings, such as Binding to StaticResource
s, DynamicResource
s, and elements. Luckily for us, elements are the simplest to bind to. To bind to an element, we need to give the Binding
markup an ElementName
and a Path
. The ElementName
is the unique name (or x:Name
) of the element we wish to bind to. Path
is the property of that element we want to bind to at run-time. It uses Reflection at run-time to bind to the ElementName
passed in, and then looks at the value of the Path
of that object and binds to it.
Be careful!!! If you give an incorrect Path
, you may have a hard time digging up the error because it doesn't throw an exception per-say. Instead, you'll get a message printed to the Output window of the Visual Studio, stating something like:
"System.Windows.Data Error: 35 : BindingExpression path error: 'sdf' property not found on 'object' ''ComboBoxItem' (Name='')'. BindingExpression:Path=SelectedItem.sdf; DataItem='ComboBox' (Name='ComboBoxColors'); target element is 'Border' (Name=''); target property is 'Background' (type 'Brush')"
You can obviously see that "sdf
" is not a property of ComboBoxItem
.
So, let's do the Binding
, shall we?
<Border Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=SelectedItem.Content}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=SelectedItem.Content}" />
Voila! We set the Background
property of each Border
to the current Content
of the SelectedItem
of the ElementName
, which in this case is our ComboBox
.
Now, if you run the application and change the ComboBox
, you'll see the Border
s change. Notice how we still haven't written one smidgen of code in our code-behind yet... This is really a powerful language that separates business logic from the user-interface. Ideally, you would have a collection of Color
s (or bind to the pre-defined enum of colors that is part of the mscorelib
of the System
namespace).
That completes our simple XAML to XAML element data-binding example.
Binding XAML Defined Controls to Code-Behind
This example is going to be short, but simple. We're going to change our ComboBoxColors
element to grab its data from C# instead of defining its children in XAML.
This is actually fairly easy. Let's take it one step at a time. First, we can remove the three ComboBoxItem
s from ComboBoxColors
since we'll be defining these in the code-behind for the Window
.
Now, we should have:
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True"> </ComboBox>
or
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True" />
Now, let's create an enum
that contains our colors.
namespace
{
JumpStartWPFpublic enum Colors
{
DarkRed,
DarkGreen,
DarkBlue
}
class Window1 : Window
{
{
InitializeComponent();
}
}
}
public Window1()
It took me forever to figure out, but you must create your enum
s to be outside of the class, and public
. Just do it, you'll thank me later.
Now, we're going to set the ItemsSource
of our ComboBox
to all the values of Colors
.
namespace
{
JumpStartWPFpublic enum Colors
{
DarkRed,
DarkGreen,
DarkBlue
}
{
{
InitializeComponent();
ComboBoxColors.ItemsSource =
}
}
}
public Window1()Enum.GetNames(typeof(Colors));
There are a few ways to bind a WPF element to the C# code-behind, this is the easiest for me. All this is doing is setting the Item
collection ItemSource
to all the values of the target enum. In this case, it's going to fill our ComboBox
with DarkRed
, DarkGreen
, and DarkBlue
at run-time. Notice how I put this code after InitializeComponent()
. If you put it before, bad things happen. Again, you'll thank me later.
Now, if you run the application, you'll notice our ComboBox
is filled with Item
s of type Colors
(our enum) correctly. But wait... our Border
's Background
s aren't displaying! Well, this is because our ComboBox
is no longer containing a list of ContentControl
s. Now, we are physically binding our ComboBox
's ItemsSource
to a String[]
(returned from Enum.GetNames()
). Well, to Bind
to a String
, you simply need to point the Path
of your target to "Text".
So, make this change, and voila, you should have working Border
s that are bound to the ComboBox
's SelectedItem
, of type String
.
<Border Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
This completes our example of Binding a XAML defined element to C# code-behind. We successfully populated a ComboBox
with a code-behind defined enum, and bound our Border
's Background
property to whatever was currently selected in the ComboBox
.
Displaying Custom Content (Business Object C# Types) in XAML
Our next goal is to add a user input form that asks for first name, last name, and age. It stores each of these objects as type Person
, into a collection of Person
s of type People
. Then, we'll have a custom list that displays all of this information.
The topics covered in this section will be:
- XAML Resources
- Displaying custom user created classes in XAML
ItemsControl
s (I.e.: displaying all information of a type Person
, into a single ListBox
)
ObservableCollection
s and how WPF uses it
- StaticResource Binding
Most tutorials I see out there fail to go into the details about how these classes are made. The design of custom classes in WPF is very important, and I'm going to document every step in here so you don't miss out on something.
First, for this, I'm going to create a class called People
that is an ObservableCollection
of type Person
, which will contain properties for first name, last name, and age.
So, create a new class called Person
.
Add public properties for FirstName
, LastName
, and Age
(Int32
) (I also make all classes that are going to be accessed in XAML public
).
namespace JumpStartWPF
{
public class Person
{
public Person(String firstName, String lastName, Int32 age)
{
FirstName = firstName;
LastName = lastName;
Age = age;
}
public Int32 Age
{
get;
set;
}
public String LastName
{
get;
set;
}
public String FirstName
{
get;
set;
}
}
}
Simple enough... Now, let's create a class that will be the List
of type Person
. Let's call it People
.
Remember, our goal is to have it so whenever a user enters their information and clicks a Submit button, we are going to create a new Person
and add it to our list of Person
s, which we're calling People
.
Now, how are we going to do this? There is a very nifty base class called ObservableCollection<Type>
.
Basically, any class that derives from ObservableCollection<Type>
automatically obtains the functionality of being a List
of the type you declare in the ObservableCollection
. So, WPF simply needs a reference to People
, and it will automatically be updated whenever the People
ObservableCollection
receives a new object of type Person
added to it.
ObservableCollection
resides in the namespace:
using System.Collections.ObjectModel;
namespace JumpStartWPF
{
public class People : ObservableCollection<Person>
{
public People() { }
}
}
Since People
inherits from ObservableCollection
, we automatically get the List
that it contains.
For the first part of our example, I'm going to manually add some Person
s since it's easier.
public class People : ObservableCollection<Person>
{
public People()
{
}
}
We call the parent method Add
, which is part of ObservableCollection
. It basically just adds a new object of type Person
(which is what our ObservableCollection
was expecting), to the List
declared inside the parent ObservableCollection
.
Alright, before I move on... we need to reorganize our elements in the window to give room for our next task. So, I've just made a lot of aesthetic changes, nothing too major.
I'm going to post the new code here...
<Window x:Class="JumpStartWPF.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="3*" />
<RowDefinition Height="3*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10*" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border x:Name="BorderLeft" Grid.Column="0" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Border Grid.Column="3" Grid.RowSpan="4"
Background="{Binding ElementName=ComboBoxColors, Path=Text}" />
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit"/>
</MenuItem>
</Menu>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Top"
HorizontalAlignment="Center">
<Label Content="Choose a color" />
<ComboBox x:Name="ComboBoxColors" SelectedIndex="0"
IsSynchronizedWithCurrentItem="True">
</ComboBox>
</StackPanel>
<Border Grid.Row="1" Grid.Column="1" Margin="0,30"
HorizontalAlignment="Center"
VerticalAlignment="Bottom"
BorderBrush="{Binding ElementName=BorderLeft, Path=Background}"
BorderThickness="1"
CornerRadius="10">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="First Name: " Grid.Row="0" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBlock Text="Last Name: " Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBlock Text="Age: " Grid.Row="2" Grid.Column="0"
HorizontalAlignment="Right"
VerticalAlignment="Center" Margin="0,0,15,0"/>
<TextBox Grid.Row="0" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="90"/>
<TextBox Grid.Row="1" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="90" />
<TextBox Grid.Row="2" Grid.Column="1" HorizontalAlignment="Left"
VerticalAlignment="Center" Width="50"/>
</Grid>
</Border>
<Border Grid.Row="1" Grid.Column="1"
BorderBrush="{Binding ElementName=BorderLeft,
Path=Background}"
BorderThickness="1" CornerRadius="5" Margin="60,5"
HorizontalAlignment="Center"
VerticalAlignment="Bottom" Width="100">
<Button Margin="1" Click="Button_Click">
<Button.ContentTemplate>
<DataTemplate>
<ContentControl Content="Submit"/>
</DataTemplate>
</Button.ContentTemplate>
</Button>
</Border>
</Grid>
</Window>
There are a couple of new concepts in all this markup that I've introduced. I'll go over them now.
First, even though I didn't want to, I ended up using a Grid
for the layout of the User Information form. The stack panel was not agreeing with me today, so I setup a couple of columns and three rows. I also moved it all up into the second row of the entire window. This will give us room to display information in the other two rows.
Onto the new concepts.
First, you'll notice a Button
element. The way I've displayed the Button
gets to another powerful feature of WPF. All ContentControl
s and ItemsControl
s are customizable. As you can see here, the only Button
aspect to this Button
is the declaration of Button
. Everything else from its border to its content is all custom.
I surrounded the Button
with a Border
only because I like the way Border
s look, and I treat it like a Panel for my Button
which is its child. I also bound the BorderBrush
of this Border
to the Background
of the Border
elements on the left and right of my Window
. This is just a cool feature that I wanted to keep concurrent throughout the entire application. Then, I declare a button, but instead of setting the Content
directly in the Button
declaration, I tell the Button
element that I want to go into more detail. Instead of setting the Content
, I open up the Button.ContentTemplate
tag. Within there, pretty much all you can do is define the DataTemplate
. DataTemplate
s are a huge topic in WPF, and I could spend hours talking about it. All I'm going to say is, consider DataTemplate
s the makeup of an element. When you define the DataTemplate
, you are literally re-teaching WPF how to draw this specific element. This is also how developers keep a theme to their applications. For instance, instead of continually setting all my Border
's BorderBrush
properties to bind to a target element, I would normally declare a DataTemplate
for all Border
s in my Window Resource, and say, whenever my Window
comes across a Border
, set its DataTemplate
to this Resource and it will handle all the cosmetic changes I want it to display.
So, for this button's example, all I'm doing is teaching the button to do what it already knows how to do. I'm just adding a ContentControl
(this is the base class control for all single item Content Controls... such as TextBlock
s and Button
s and Label
s). I only expanded on the DataTemplate
so I could explain a little about its use. I will actually define a DataTemplate
in a Window Resource later on for use.
Also note, I created a Click
event for the Button
. When you do this, if you have Intellisense hooked up, it will automatically create the event handler for whatever event you just registered. In this case, my code-behind was edited with this:
private void Button_Click(object sender, RoutedEventArgs e)
{
}
Those were the only new concepts in my big cosmetic change. Now, we can get back to the goal. In-case you forgot, the goal we're trying to achieve is whenever you click Submit
, it will add a new Person
to our People
ObservableCollection
, and then we'll have some sort of ItemsControl
in our Window
that is bound to the People
collection, so it will automatically pick up newly added Person
s and display them.
Let's take this one step at a time. First, we know that if we want to add a new Person
, we'll need to somehow gather the information that was entered into the TextBox
es. In order to get those TextBox
es' content, we'll need to give them unique names (just like we did with our first ComboBox
a while back). Let's do that now.
<TextBox x:Name="TextBoxFirstName" Grid.Row="0" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="90"/>
<TextBox x:Name="TextBoxLastName" Grid.Row="1" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="90" />
<TextBox x:Name="TextBoxAge" Grid.Row="2" Grid.Column="1"
HorizontalAlignment="Left" VerticalAlignment="Center" Width="50"/>
Simple enough. I just gave them obvious names so we can look them up in the C# side when adding a new Person
.
Now, let's hook up our click to add a new Person
to the People
collection. I think our People
collection should have a static access modifier so we don't continue creating new People
collections each time we add a new Person
to it from different sources. To do this, I simply created a static instance of the People
class, and set it in the constructor to "this
".
public class People : ObservableCollection<Person>
{
public static People Instance;
public People()
{
Instance = this;
}
}
Now, we can call this code from our Button
click:
private void Button_Click(object sender, RoutedEventArgs e)
{
if (People.Instance.Count >= 5)
{
People.Instance.RemoveAt(0);
}
People.Instance.Add(new Person(TextBoxFirstName.Text, TextBoxLastName.Text,
Int32.Parse(TextBoxAge.Text)));
}
So, what I'm doing here is limiting the amount of Person
s our People
collection will maintain. If it gets beyond 5, we clear out the oldest person and move the second oldest one up. This is a first in first out collection (FIFO). Then, I get access to the current People
instance by accessing the static accessor, and call the Add
method which is part of the ObservableCollection
base class, passing in our first name, last name, and age (converted to an int
).
WPF provides error validation on elements through the use of another interface that can be implemented, called IDataErrorInfo
. This is out of the scope of this article for now, but ideally, you'd want some sort of checking to be done on the input coming in from the user on the TextBox
es. Perhaps even some Converter
s, the possibilities are endless.
OK, now we need to create a visual collection of these People
.
<StackPanel Grid.Row="2" Grid.Column="1" HorizontalAlignment="Center"
VerticalAlignment="Center">
<Border BorderBrush="{Binding ElementName=BorderLeft, Path=Background}"
BorderThickness="1" CornerRadius="2">
<ItemsControl x:Name="PeopleItemsControl"
ItemsSource="{StaticResource PeopleCollection}"
ItemTemplate="{StaticResource PersonItemTemplate}" />
</Border>
</StackPanel>
So here, I created a vertical (by default) StackPanel
. This is going to be the Container
for each Person
. Within it, I put a Border
to go around the entire ItemsControl
. I then put a generic ItemsControl
. Remember, ItemsControl
is simply a base class for a collection of Item
s. I usually use it when I want to really define what I want my collection to look like. In this case, I didn't want the user to be able to select an item, or have any highlight/actions on an item, so I didn't want to use a ListBox
or a ComboBox
. I just simply wanted to display the item.
Now you should notice a few new concepts in here. First, the ItemsSource
. Remember how our last ComboBox
was set in the code-behind? Well, I'm really just doing the same thing here, but this time, I'm setting it to a StaticResource
in my XAML file. What's a Resource? Well, it's basically an object created in either C# or XAML, but is instantiated in XAML itself. (Note: It is possible to declare a resource in the code-behind and have it usable as a Static or Dynamic Resource in XAML, but for simplicity's sake, our Resource is defined in XAML).
Now, onto the syntax. Something that boggled my mind for a while was when to use and when not to use the Binding
markup. I'm still not 100% comfortable with it, but I know that I don't use Binding
markups when I am binding my collection controls to a Resource. So where is this resource I called PeopleCollection
? Well... it's up in the Resources section.
<Window.Resources>
<local:People x:Key="PeopleCollection" />
</Window.Resources>
If you simply add this, it won't compile. local
doesn't exist by default. You have to create it. local
, in this case, is the name I gave my class in People.cs. Remember how that class is an ObservableCollection
by proxy, so if I create an instance to that class, I am really also getting the entire collection that goes along with it. Here's how you add a Resource.
The top of the window contains a bunch of namespace declarations. Well, if you want to get access to your own types, you need to declare your namespace up here too.
The syntax is, "xmlns
" followed by a colon ":", followed by an optional key for your namespace, followed by "clr-namespace
", followed by a colon ":", followed by your namespace. From the beginning of "clr-namespace
" to the end of your namespace, it is surrounded in quotes.
The optional key you can give your namespace is very important... it's how you reference the classes within that namespace when declaring a resource for your Window
. I usually name the namespace that my main XAML window is contained in, "local
".
xmlns:local="clr-namespace:JumpStartWPF"
When you go back to your Window.Resources
, you will notice that People
should exist now. All Resources must have a key associated with them or some other type of associated key (such as a TargetType
etc.). You can consider creating a Resource to your class the same as creating a new instance of your class. When this is called, it will actually create a new instance of our People
class, and activate our static instance for use.
Now, back to our ItemsControl
. There are two types of Resource bindings that you can do: StaticResource
and DynamicResource
. I've never used DynamicResource
, but the way it works is if the Resource ever changes at run-time, all elements referencing that Resource will change too. StaticResource
is a one time bind. DynamicResource
is a little slower, since it can keep creating new references, while static lives on forever in the application lifetime.
Now on to one of our last topics.. the ItemTemplate
property of our ItemsControl
. Remember how we set our Submit button's DataTemplate
a while back? The DataTemplate
was telling our Button
how to display itself. All we did was set the Content
of the Button
to Submit, but that's a lot of information about WPF to take in at once.
We're going to expand upon that now, and actually completely customize our ItemsControl
and teach it how to display a Person
.
You can already tell that I've bound to a StaticResource
of PersonItemTemplate
. ItemTemplates
/ContentTemplates
point to DataTemplate
s. So, you should be able to figure out that, in our Resources section, we have declared a DataTemplate
with the key PersonItemTemplate
.
Then, whenever our ItemsControl
needs to draw a Person
, it refers to that StaticResource
to the DataTemplate
of how to draw a Person
.
<DataTemplate x:Key="PersonItemTemplate">
<Border BorderBrush="Black" BorderThickness="1" CornerRadius="10">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Margin="5,0,0,0" Text="{Binding LastName}" HorizontalAlignment="Center" />
<TextBlock Text=", " HorizontalAlignment="Center" />
<TextBlock Margin="0,0,5,0" Text="{Binding FirstName}" HorizontalAlignment="Center" />
</StackPanel>
<TextBlock Margin="0,0,0,5" Text="{Binding Age}" HorizontalAlignment="Center" />
</StackPanel>
</Border>
</DataTemplate>
Remember, before we set a Border
to surround the entire ItemsControl
? Well now, I want an even more round Border
(CornerRadius
set to 10) to surround each individual Person
. Within that, I have a vertical StackPanel
that contains a horizontal StackPanel
with a person's LastName
, separated by a comma, and then a space and their FirstName
. Then, below that StackPanel
is a TextBlock
displaying the Person
's age. Notice the {Binding
...} markups for each TextBlock
. At compile time, our DataTemplate
here has absolutely no idea what a Person
is. So how does it know what LastName
, FirstName
, and Age
are? The answer is Reflection. At run-time, whenever it comes time to use this DataTemplate
, the DataTemplate
will inherit all properties of the calling control. In this case, our calling control is an ItemsTemplate
with its ItemsSource
set to type People
, which is a collection of type Person
, which has the properties FirstName
, LastName
, and Age
. See how it all works? We're piggy backing on the glory of inheritance.
Well, we forgot one thing now, didn't we? Adding the Click
event for the Exit MenuItem
off the main menu. You should be able to easily do this now with your knowledge on the topics above.
The code is given below:
<Menu VerticalAlignment="Top" Grid.ColumnSpan="3">
<MenuItem Header="File">
<MenuItem Header="Exit" Click="MenuItem_Click"/>
</MenuItem>
</Menu>
private void MenuItem_Click(object sender, RoutedEventArgs e)
{
if (People.Instance != null)
{
People.Instance.Clear();
People.Instance = null;
}
this.Close();
}
Thanks for reading! Again, I just started WPF about a week ago. Some of my information may be worded incorrectly, or there may be better methods of achieving the same tasks I did. All-in-all, I feel that this is a good introduction to WPF, and it offers some information that I felt was difficult to figure out on my own. Hope it helps!