Part 5: Basics of Layout and Events
In this lesson I want to talk about layout, or in other words, how controls are positioned and arranged on your app's user interface.
So, my game plan:
- We'll start by talking about the two primary elements used in layout and positioning: the Grid element and StackPanel element.
- With regards to the Grid, I'll talk about defining rows and columns and various sizing options and techniques you can use to fully unlock the power of the Grid.
- Next, we'll learn about the StackPanel and just for fun, we'll convert our app from a Grid layout to a StackPanel layout—this will teach us about some key differences between the Grid and StackPanel.
- Finally, we'll talk about how event handers are "wired up" from both XAML and in C#.
1. Understanding the Basics of Grids
The default Windows Phone Page template creates a <Grid> element called "ContentPanel".
The Grid element is used for laying out other controls ... it allows you to define rows and columns, and then each control can request which row and column they want to be placed inside of.
Now, in the default MainPage.xaml page template, between the opening and closing <Grid></Grid> tags, the Grid appears empty ... it appears that there's no rows or columns defined. However, by default, there's always one RowDefinition and one ColumnDefinition even if it's not explicitly defined, and these take up the full vertical and horizontal space available to represent one large "cell" in the Grid. Any items placed between the opening and closing <Grid></Grid> elements are understood to be inside that single implicitly defined "cell".
2. Grid RowDefinitions and ColumnDefinitions and defining sizes
An example of a Grid that actually defines rows is the "LayoutRoot" grid. The Grid defines two rows:
For now, notice how the StackPanel with the project and page titles places itself inside the first row ... the StackPanel has an attribute called Grid.Row="0". You reference both rows and columns using a zero-based numbering scheme.
Notice how the ContentPanel Grid places itself in the second row (by setting the attribute Grid.Row="1").
The first row has its height set to "Auto". The second row (row 1) is set to "*". There are three syntaxes that you can use to help persuade the sizing for each row and column. I used the term "persuade" intentionally. With XAML layout, heights and widths are relative and can be influenced by a number of factors. All of these factors are considered by the layout engine to determine the actual placement of items on the page.
For example, "Auto" means that the height for the row should be tall enough to accommodate all of the controls that are placed inside of it. If the tallest control is 150 pixels tall, then that's the actual height of the row. If it's only 100 pixels, then THAT is the height of the row. Therefore, "Auto" means the height is relative to the controls inside of the row.
The asterisk is known as "star sizing" and means that the height of the row should take up all the rest of the height available.
Here's a quick example of another way to use "star sizing" ... I created a project that has three rows defined in the ContentPanel. Notice the heights of each one:
Putting a number before the asterisk, I'm saying "Of all the space available, give me 1 or 2 or 3 shares of the remainder". Since the sum of all those rows adds up to six, each 1* is equivalent to 1/6th of the height available. Therefore, 3* would get half of the height available as depicted in the output of this example:
Besides Auto and star sizing, you can also specify widths and heights (as well as margins) in terms of pixels. In fact, when only numbers are present, it represents that number of pixels. Generally, it's not a good idea to use exact pixels in layouts for widths and heights because of the likelihood that screens—even Windows Phone screens—can have different dimensions. Instead, it's preferable to use relative values like Auto and star sizing for layout.
One thing to note from this example (and from our original PetSounds example) is that control widths and heights are assumed to be 100% unless otherwise specified. That's why the rectangles take up the entire "cell" size. This is why the button first occupied the entire ContentPanel grid, and why I had to specify I wanted the button to be 200 x 200 pixels instead.
I want to also point out that a Grid can have a collection of ColumnDefinitions as you can see from this example app I created called GridsRowsAndColumns. Here we have a 3 by 3 grid:
... the result is a simple grid with a number in each "cell":
The other thing I want you to notice about this example is that when you don't specify a Grid.Row or Grid.Column, it is assumed to mean the first row (row 0) or first column (column 0). Relying on the defaults keeps your code more concise.
3. Grid cell Alignments and Margins
I have another example app called AlignmentAndMargins. This XAML:
Produces this result:
Most of this example should be obvious if you stare at it for a few moments, but there are several finer distinctions I want to make about Alignments and Margins. First, it points out how VerticalAlignment and HorizontalAlignment work, even in a given Grid cell (and the same will hold true in a StackPanel as well). The ___Alignment attributes PULL controls TOWARDS their boundaries. By contrast, the Margin attributes PUSH controls AWAY from their boundaries. The second observation is the odd way in which Margins are defined ... as a series of numeric values separated by commas. This convention was borrowed from Cascading Style Sheets. The numbers represent the margin pixel values in a clock-wise fashion starting from the left side. So,
Means, 10 pixels from the left boundary, 20 pixels from the top boundary, 30 pixels from the right boundary, 40 pixels from the bottom boundary. In this case, the boundary is a single Grid cell (since the Content panel only has not defined any additional RowDefinitions and ColumnDefinitions).
A bit earlier I said that it's generally a better idea to use relatives sizes like Auto and * to define heights and widths ... why, then, are margins are defined using pixels? Margins are expressed in exact pixels because they are usually just small values to provide spacing or padding between two relative values and can therefore be a fixed value without negatively impacting the overall layout of the page.
4. StackPanel basics
The second style of layout is enabled by using the StackPanel element. It will arrange your controls in a flow from top to bottom.
Back in our PetSounds project, we have the following StackPanel defined to create the app title and page title at the top of the MainPage.xaml:
Both TextBlock elements take up the entire horizontal width of their parent (the LayoutRoot Grid) and therefore they are stacked vertically. Let's convert our ContentPanel:
from a Grid into a StackPanel:
At first, this conversion only moves the Meow Button vertically downward:
However, if I were to remove the Width and HorizontalAlignment attributes from the Quack button (I also remove the Button.Background property):
And comment out the Width and HorizontalAlignment attributes from the Meow button ... AND comment out the Margin in my MainPage() constructor:
Then you could see that the two buttons are vertically stacked in the StackPanel:
So, as you can see, you can achieve just about any layout you can imagine by using a combination of four things:
- Grid layout, including RowDefinitions and ColumnDefinitions
- StackPanel layout
- Margins to move elements away from the left-side, top-side, right-side or bottom-side
- The HorizontalAlignment and VerticalAlignment attributes
Ok, so that's XAML Layout in a nutshell. Let's move on to events.
5. Understanding Events
If you watched the C# Fundamentals series on Channel9, in one of the last videos we talked about events. In the Windows Phone API, Pages and Control raise events for key moments in their lifecycle OR for interactions with the end user. In this series, we've already "wired up" the Click event of our PlayAudioButton with a method that I called an "event handler". When I use the term "event handler" I'm merely referring to a method that is associated with an event. I use the term "wired up" to mean that the event is tied to a particular event handler method. Some events events can be triggered by a user, like the Click event of a Button control. Other events happen during the lifetime of an event, like a Loaded event that occurs after the given Page or Control object has been instantiated by the Phone APIs Runtime engine.
There are two ways to "wire up" an event for a Page or Control to an event handler method. The first is to set it using the attribute syntax of XAML. We've already done this in our PlayAudioButton example:
If you'll recall, we typed:
And before we typed in the closing double-quotation mark, Intellisense asked if we wanted to select or create an event handler. We told it we wanted to create a new event handler, and Visual Studio named the event handler using the naming convention:
... so in our case ...
Visual Studio also created a method stub in the code-behind for the XAML page, in our case, the MainPage.xaml.cs. Keep in mind that these are Visual Studio automations. We could have hit the escape key on the keyboard when Intellisense first popped up and typed that code in by hand.
A second way to associate an event with an event handler is to use the Properties window in Visual Studio:
(1) First, you must make sure you've selected the Control you want to edit by placing your mouse cursor in that element. The name of that element will appear in the Name field at the top letting you know the context of the settings below. In other words, any settings you make will be to the PlayAudioButton as opposed to another control on the page.
(2) Click the lightening bolt icon. This will switch from a listing of Properties / attributes that can be changed in the Properties window to the Events that can be handled for this Control.
(3) Double-click on a particular textbox to create an association between that Control's event and an event handler. Double-clicking will automatically name the event and create a method stub for you in the code behind.
The third technique (that we'll use several times in this series) is to wire up the event handler in C# code. See line 35 below for an example:
The += operator can be used in other contexts to mean "add and set" ... so:
x += 1;
... is the same as ...
x = x + 1;
The same is true here (in a sense) ... we want to "add and set" the Click event to one more event handler method. Yes, the Click event of myButton can be set to trigger the execution of MULTIPLE event handlers! Here we're saying "When the Click event is triggered, add the PlayAudioButton_Click event handler to the list of event handlers that should be executed." One Click event could trigger on or more methods executing. In our app, I would anticipate that only this one event handler, PlayAudioButton_Click, would execute.
You might wonder: why doesn't that line of code look like this:
myButton.Click += PlayAudioButton_Click();
Notice the open and closed parenthesis on the end of the _Click. Recall from the C# Fundamentals series ... the () is the method invocation operator. In other words, when we use () we are telling the Runtime to EXECUTE the method on that line of code IMMEDIATELY. That's NOT what we want in this instance. We only want to associate or point to the PlayAudioButton_Click method.
One other interesting note about event handler methods. With my addition of line 35 in the code above, I now have two events both pointing to the same event handler method PlayAudioButton_Click. That's perfectly legitimate. How, then, could we determine which button actually triggered the execution of the method?
If you look at the definition of the PlayAudioButton_Click method:
private void PlayAudioButton_Click(object sender, RoutedEventArgs e)
The Windows Phone Runtime will pass along the sender as an input parameter. Since we could associate this event with any Control, the sender is of type Object (the type that virtually all data types in the .NET Framework ultimately derive from), and so one of the first things we'll need to do is do a few checks to determine the ACTUAL data type (Are you a Button control? Are you a Rectangle control?) and then cast the Object to that specific data type. Once we cast the Object to a Button (for example) then we can access the Button's properties, etc.
The method signature in the code snippet above is typical of methods in the Windows Phone API. In addition to the sender input parameter, there's a RoutedEventArgs type used for those events that pass along extra information. You'll see how these are used in advanced scenarios, but I don't believe we'll see them used in this series.
To recap, the big takeaways in this lesson are the ways in which we can influence the layout of Pages and Controls. With regards to Grid layout we learned about the different ways to define the heights and widths of Rows and Columns (respectively) using Auto sizing, star sizing and pixel sizing. We talked about how VerticalAlignment and HorizontalAlignment pull controls towards boundaries while Margins push Controls away from boundaries. Then we talked about the different ways to wire up events to event handler methods, and the anatomy of an event handler method's input parameters.