Você está na página 1de 21

Controls

Silverlight 2 has over 3 dozen User Interface (UI) controls. .NET programmers already familiar
with ASP.Net or (especially) WPF, will find using the Silverlight controls very natural and
straight forward.

Figure 1-1. Silverlight Controls (split in two to make it easier to view)

Silverlight controls were created to look great and provide extensive and customizable
functionality right out of the box. Moreover, all of the standard controls can be modified in
numerous ways to meet your needs.

The look and feel of the control can be tweaked through styles or can be entirely redesigned
through templates, and the behavior of the controls can be modified through event handlers. In
the rare cases when none of that is enough, you can create (or derive) your own customized
controls as well.

This tutorial will cover event handlers. Styles, Templates and Custom Controls will be covered
in future tutorials. .

A Note On The Design Surface


The current Visual Studio design surface is read-only. That is, you can drag and drop controls
into the markup (or write the Xaml by hand) and the effects are seen immediately in the design
surface, but you cannot yet drag or otherwise manipulate controls directly in design mode.

That is a temporary limitation. For now, one alternative is to use Expression Blend, and we have
a full tutorial on Blend for Programmers that shows how to work with the two environments
together.

Note that controls can also be created dynamically (in code, at run time) as explained at the end
of this tutorial.

Layout Controls

The use of layout controls is both straight-forward and essential to the creation of Silverlight
applications. Layout controls are used to manage the placement of other controls (including other
layout controls!) in your Silverlight application. You can think of the layout controls as
"containers."

The three Layout controls you'll use most often are:

• Grid - essentially a table used for positioning objects in rows and columns.
• StackPanel - used to position objects next to one another, or atop one another.
• Canvas - used for absolute positioning (and unchanged from Silverlight 1.0)

They are listed in the order of how frequently they are used. And to be honest, if you learn Grid
and StackPanel, you can probably program for a long time on just those two.

Grids

Grids (not to be confused with DataGrids) offer easy and exact placement by providing a table-
like structure. You declare rows and columns, and then place controls into a specific row/column
location (spanning across rows or columns as needed).

While you can tweak your grids to achieve very precise placement, the basic use of grids is
extremely straight forward.
You typically declare a grid, declare its rows and columns and then start placing controls into
specific cells (e.g., column 1, row 3). To see this at work, let's create our first Silverlight
Application: Easy Grid.

Creating The First Example - A Grid With Controls


Open Visual Studio 2008 and click on Create Project and then in the new project window you'll
want to create a C# project using the Silverlight Application Template.

Pick a location for your application and give it a meaningful name; be sure that you are building
against the latest framework.

Figure 1-2. Creating A New Silverlight Project

When you click OK you will next be asked if you'd like to generate a Web Site or a Web
Application (using the top radio button) or just a test page (using the bottom radio button) or if
you'd like to link to an existing web site; all as shown in the next figure.
Figure 1-3. Choose Simple HTML Test Page

If you create just a test page, the project is kept very simple, but if you generate a Web Site or
Web Application Project, then Visual Studio creates two projects in your new solution; the
Silverlight Application and a test application; excellent for test-based programming but more
than we need right now.

Choose the second radio button (Test Page) and let Visual Studio create a simple application for
you. If Page.xaml doesn't open automatically, double click on it in the Solution Explorer. You
should find that Visual Studio has guessed that you want a grid as your main container, and has
created one for you and named it LayoutRoot. (Also note that the very first declaration in each
"page" is a UserControl.

<UserControl x:Class="EasyGrid.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">

In fact a Page is a UserControl, and we'll return to that relationship in a later tutorial.

The Grid definition created by Visual Studio 2008 looks like this:

<Grid x:Name="LayoutRoot" Background="White">


</Grid>
Many times, your entire application will go between the opening and closing Grid tags; other
times you’ll replace the Grid with another container.

Defining Rows And Properties

You define the Rows for a table inside the Grid.RowDefinitions element. For each Row, you add
a RowDefinition element, which itself may have various properties, including a specific height,
or if you prefer you may set the height to be proportional to the available space or to take all the
space not taken by other rows.

<Grid x:Name="LayoutRoot" Background="White">


<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="30*" MaxHeight="70" />
<RowDefinition Height="40*" MaxHeight="70" />
<RowDefinition Height="*" MinHeight="30" MaxHeight="50" />
<RowDefinition Height="Auto" MinHeight="5" MaxHeight="30" />
</Grid.RowDefinitions>
</Grid>

Height="Auto"

With Auto, the Grid’s space is distributed evenly based on the size of the content within the row.

Star or Proportional Sizing

In proportional sizing the value of a column or row is expressed in Xaml as *.

<RowDefinition Height="*"

However, you can give twice the space to one column or row as another by using 2* (or a 5:7
ration by using 5* and 7*).

<RowDefinition Height="30*"
<RowDefinition Height="40*"

If you combine this with a Minimum or Maximum Height you get finer control over the limits of
the row's range of sizes,

<RowDefinition Height="30*" MaxHeight="70" />


<RowDefinition Height="40*" MaxHeight="70" />
<RowDefinition Height="*" MinHeight="30" MaxHeight="50" />
Minimal or Controlled space

By default child elements of grid take up the least amount of space necessary to to accommodate
the largest content within a cell in a given row or column. You can take greater control over
positioning, however, by using the margin and alignment properties as described below

Sizing Units

To provide the most flexibility, Grid columns and rows are sized by GridLength objects which
use the GridUnitType, which in turn allows you to choose among:

• Auto (size based on the size properties of the object being placed in the grid)
• Pixel (size in pixels)
• Star (size based on a weighted proportion of the available space)

designating, if we choose, the minimum and maximum dimensions of each.

To see the effect of all this we'll create five rows using different sizing rules. We'll also create
three columns with no sizing rules at all (!). We'll then fill our grid with controls and take a look
at some of the effects. Here is the code, which we'll take apart piece by piece.

Example 1-1. The Xaml for EasyGrid

<UserControl x:Class="EasyGrid.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="30*" MaxHeight="70" />
<RowDefinition Height="40*" MaxHeight="70" />
<RowDefinition Height="*" MinHeight="30" MaxHeight="50" />
<RowDefinition Height="Auto" MinHeight="5" MaxHeight="30" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>

<TextBlock x:Name="FirstNamePrompt "


Grid.Row="0" Grid.Column="0" Text="First Name:" Margin="5"/>
<TextBox x:Name="FirstName"
Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"
Width="150" Background="Bisque" Margin="5" />

<TextBlock x:Name="LastNamePrompt "


Grid.Row="1" Grid.Column="0" Text="First Name:" Margin="5"/>
<TextBox x:Name="LastName"
Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="2"
Width="150" Background="Beige" Margin="5" />

<TextBlock x:Name="SubscriberPrompt"
Grid.Row="2" Grid.Column="0"
Text="Subscriber?" VerticalAlignment="Bottom" />
<CheckBox x:Name="PlanA" Grid.Row="2" Grid.Column="1"
Content="Plan A" IsChecked="true"
VerticalAlignment="Bottom" />
<CheckBox x:Name="PlanB" Grid.Row="2" Grid.Column="2"
Content="Plan B" VerticalAlignment="Bottom" />

<TextBlock x:Name="Hello" Text="Hello"


Grid.Row="3" Grid.Column="2" VerticalAlignment="Bottom"/>

<TextBlock x:Name="World" Text="World"


Grid.Row="4" Grid.Column="2" VerticalAlignment="Bottom"
FontFamily="Comic Sans MS" FontSize="24"
FontWeight="Bold" Margin="0,20,0,0"/>
</Grid>
</UserControl>

The result of the Xaml code is shown in

Figure 1-4. The Xaml as Design

First, it is important to understand that the Xaml shown in Example 1-1 is all that is needed to
produce the Silverlight control shown in Figure 1-4. It is true that you can't see the rows and
columns, but that is easily remedied. Find the declaration of the Grid and set its ShowGridLines
property to True (Intellisense will help as shown inFigure 1-5
Figure 1-5. Adding Property ShowGridLines and setting it True

When you do, the grid lines become visible; which can be very useful during design, as long as
you remember to set the property to false before you post your Silverlight application!

Figure 1-6. ShowGridLines Makes The Alignment Very Visible

Unpacking the Xaml


The first lines create the Grid and define how the rows will share the space.

<Grid x:Name="LayoutRoot" Background="White" ShowGridLines="True" >


<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="30*" MaxHeight="70" />
<RowDefinition Height="40*" MaxHeight="70" />
<RowDefinition Height="*" MinHeight="30" MaxHeight="50" />
<RowDefinition Height="Auto" MinHeight="5" MaxHeight="30" />
</Grid.RowDefinitions>

As described above, the RowDefinitions define five rows. The first has a fixed height of 50. The
second and third will float, but in a 3:4 proportion with one another, but each with a maximum
height of 70. The next row will take all the remaining space, but will never be allowed to shrink
to less than 30 nor grow to more than 50. The final row will size itself to the object placed in the
row, but yet is constrained to no less than 5 and no more than 30.

After the column definitions we define a TextBlock and a TextBox. We'll discuss these controls
in more depth later, but the former is typically used as a label and the latter is used to allow the
user to put in text.

<TextBlock
x:Name="FirstNamePrompt "
Grid.Row="0"
Grid.Column="0"
Text="First Name:"
Margin="5"/>

The TextBlock has five parameters. They can be aligned one below the other, or strung out all in
a line, any way you like. The first parameter is the name (FirstNamePrompt) and is used to
allow you to address the object in code. If you give an object in Xaml a name, and save the file,
that object is available with no further declaration in your methods

Thus, if you save this file, you will be free to write

FirstNamePrompt.Text=”Hello”

From any method of this class, and the compiler will know exactly what you mean!

The Grid.Row and Grid.Column properties are called extended properties and are really
"borrowed" from the immediately surrounding grid to position the TextBlock inside the
appropriate cell.

The Text property does just what you expect, it fills the TextBlock with that text.

Margin is a fascinating property, and we'll return to it a couple times. For now, I'll mention that it
takes three forms:

• A single value, which gives a margin of that value "all around" - that is left, right, top and
bottom
• Two values in which case the first is divided evenly among the left and right margin and
the second is divided evenly among the top and bottom
• Four values, in the order Left, Top, Right, Bottom.
The TextBlock label in the first column is followed by an (input) TextBox in the second column.
The properties are very similar except that you must declare the width of your TextBox and we
choose here to give the TextBox a background color.

The next pair are much like the first.

The third row is filled by a TextBlock and two CheckBox controls. Note that they are aligned by
setting their VerticalAlignment properties and that the first CheckBox has its IsChecked property
set to True so it will be checked when the page is first shown.

The fourth row has a TextBlock that displays Hello and the final row has a TextBlock that
displays World, but does so setting the FontFamily, FontSize and FontWeight.

StackPanels

StackPanels are typically combined with other layout controls. They allow you to stack objects
one on top of the other, or next to each other (like books on a shelf).

One convenience of a StackPanel is that you do not have to provide the position of the objects
held by a StackPanel, they are positioned relative to the object declared earlier in the Stack..

In the following example, you will stack a TextBlock on top of a TextBox which in turn will sit
on top of a Button that will sit on top of a CheckBox (shades of Yertle the Turtle!)

<StackPanel Background="Beige" Orientation="Vertical" >


<TextBlock Text="Your name?"
HorizontalAlignment="Left" Margin="10,2,0,1"/>
<TextBox Width="150" Height="30"
HorizontalAlignment="Left" Margin="10,2,0,1"/>
<Button Content="Submit this information"
HorizontalAlignment="Left"
Margin="10,2,0,1" Height="30" Width="150" />
<CheckBox Content="With Zing!" HorizontalAlignment="Left"
Margin="10,2,0,1" />
</StackPanel>

There is quite a bit of information in this code snippet, so let's unpack it piece by piece.

The top and bottom lines show the declaration of the StackPanel in the Xaml file. The
StackPanel is declared with two attributes: a BackgroundColor (Beige) and an Orientation
(which must be either Vertical or Horizontal).

There are many other attributes you can set, as will be true for nearly all the controls These,
along with the methods are conveniently listed in the documentation,
Figure 1-7. Documentation Showing Stack Panel Members

By setting the Orientation to Vertical we are stacking its contents one on top of another rather
than side by side.

Within the StackPanel, the four objects are declared, and the order of their declaration will
determine the order in which they are stacked. Each is set with its TextAlignment property set to
Left so that they will align, and each has its Margin property set. Let's dig just a bit deeper into
the Margin property by looking up its definition in the documentation,

public Thickness Margin { get; set; }

The Margin property is actually an object of type Thickness. As noted earlier, when you are
declaring a Thickness object in Xaml you may do so in one of three ways:

First, you may provide a double that will be the margin for the margin on all four sides (left, top,
right, and bottom) uniformly around the object. Thus, you might write

<Button Content="Submit this information"


HorizontalAlignment="Left"
Margin="100" Height="30" Width="150" />

Thereby isolating the button with a margin of 100 on either side and above and below,
Figure 1-8. Button Margins

Notice that to accommodate the oversided margin, the width of the button was compromised!

The second way to declare a Thickness (and in this case, a Margin) is to provide the sum of the
sides and the sum of the top and bottom (thus, the sides must be equal and the top and bottom
must be equal,

<Button Content="Submit this information"


HorizontalAlignment="Left"
Margin="50,20" Height="30" Width="150" />

The effect of this declaration is that the left and right margins are 25, and the top and bottom
margins are each 10.

Finally, you may declare each independently, as long as you do so in the required order:
Figure 1-9. Margins are a Thickness Object

That is: Left, Top, Right, Bottom; or in this case, the Left margin is 10, the Top margin is 2, the
Right margin is 0 and the Bottom margin is 1.

Once the four controls are placed in the stack Panel, and aligned, the stack panel is responsible
for their placement,

Figure 1-10. Stack Panel At Work

Notice that the StackPanel is responsible for its own background color, and for stacking its
contents (the four controls) but each control is responsible for its own alignment and margins.

Horizontal Stack Panels

If we want to shift the stack panel to align all the controls into a single row, we'll want to make a
few additional changes. Not all controls default to aligning in the same way (top, center or
bottom) so we'll explicitly set their vertical alignment to "Center," just as we previously set their
horizontal alignment to "Left". Let's also set the margins to provide a bit of space between each
object as the default is to abut each object.

<StackPanel Background="Beige" Orientation="Vertical" >


<TextBlock Text="Your name?"
HorizontalAlignment="Left" Margin="10,2,0,1"/>
<TextBox Width="150" Height="30"
HorizontalAlignment="Left" Margin="10,2,0,1"/>
<Button Content="Submit this information"
HorizontalAlignment="Left"
Margin="10,2,0,1" Height="30" Width="150" />
<CheckBox Content="With Zing!" HorizontalAlignment="Left"
Margin="10,2,0,1" />
</StackPanel>

Note that I set the left margin on the text box to 5 (rather than 10) to bring it a bit closer to the
TextBlock that serves as its label.

Figure 1-11. Stack Panel in Horizontal Orientation

Control Events and Event Handlers


In Silverlight 2 each class declares whether it is supported by managed code or not, by including
(or not including) the x:Class attribute in its root element. These tutorials will assume you are
working in managed code, and that this attribute is therefore present.

When you create a managed code Silverlight application in Visual Studio, the Class attribute is
placed for you. Create a new application and call it EventHandler, and then examine the first few
lines, as shown here,

<UserControl x:Class="EventHandler.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Width="400" Height="300">

Declaring Event Handlers


There are two ways to declare event handlers in Silverlight: in the Xaml file or in the code-
behind. If you declare event handlers in Xaml you cannot add parameters. A typical event
handler declaration in XAMl might look like this
<Canvas Loaded="Canvas_Loaded">
<Button x:Name="myButton" Content="Hello"
Canvas.Left="10" Canvas.Top="10" />
</Canvas>

Here I've deleted the Grid created by Visual Studio, and declared a Canvas object. I've also
assigned an event handler named "Canvas_Loader" to the Loaded event (a pre-existing event
common to all canvases that fires when the Canvas is loaded (created).

I then declare a button in the Xaml and set its content to Hello.

This code appears in Page.xaml. In the code behind file, Page.xaml.cs, I must now implement the
event handler with the name I've promised to use,

private void Canvas_Loaded(object sender, RoutedEventArgs e)


{
myButton.Content = "Please Click Me";
}

The first thing to notice is that the name of the method is identical to that as declared in the
Xaml. The second is that this method follows the pattern of all .NET event handlers: it returns
void and takes two parameters: the first of type object (and containing a reference to the object
that raised the event) and the second of type EventArgs or a type that derives from EventArgs; in
this case RoutedEventArgs. We'll come back to RoutedEventArgs in just a moment.

The implementation is that when the Canvas is loaded it grabs the button declared in the Xaml
file and changes its Content property from "Hello" to "Please push me",

Figure 1-12. Content Property Changed

Even in this incredibly simple example there are two important things to notice:

1. You did not have to declare myButton in the code behind; it was known simply by
declaring it in the Xaml.
2. The button adjusted its size to accommodate the larger string.

Declaring Event Handlers In Code


I admit it, I have a strong preference for declaring all event handlers in code. I believe it is better
encapsulation, making for more scalable and more maintainable code. But this is a personal
opinion.

In any case, if you want to declare your event handlers correctly in code, Visual Studio makes it
very easy to do so. The most common way is to wire up the event handlers in the OnLoaded
event handler, with that event (Loaded) wired up in the Page's constructor.

To see this, return to your previous code and remove the event handler from the Xaml. While
you're there, add a check box as shown here,

<!-- <Canvas Loaded="Canvas_Loaded"> -->


<Canvas >
<Button x:Name="theButton" Content="I'm Indented!"
Canvas.Left="150" Canvas.Top="20" />

<CheckBox x:Name="RushOrder" Content="Rush"


Canvas.Left="50" Canvas.Top="20" FontSize="18" />
</Canvas>

Notice that the original Canvas is commented out, and replaced by a Canvas that does not have
an event.

Save the Xaml file and open the code behind.

In the constructor, type Lo. Intellisense will pop up and offer to help you create the
EventHandling code, landing on the event Loaded, which is exactly what you want. The tip (next
to the Intellisence box) shows the type of the event.

Figure 1-13. Intellisense Helps Create Event Handler

We'll return to the fact that Loaded is a RoutedEventHandler shortly. Press tab to accept Loaded
and type += to begin adding the delegate. (If delegates and events are new to you, you may want
to read this article. It's a bit old, but still accurate. You can also read extensively about delegates
and events in any book on .NET 3.5 or C# or VB.
Intellisense will walk you through each step of wiring up the event handler, and if you let it, will
also create the stub of the event handler method, ultimately placing your cursor in the method
which it prefills with an exception (in case you forget to add a meaningful implementation.

public Page()
{
InitializeComponent();
Loaded += new RoutedEventHandler(Page_Loaded);

void Page_Loaded(object sender, RoutedEventArgs e)


{
throw new NotImplementedException();
}

Delete the exception and register event handlers for the common events for your button (click)
and your checkbox (Checked and Unchecked)

void Page_Loaded(object sender, RoutedEventArgs e)


{
myButton.Click += new RoutedEventHandler( myButton_Click );
RushOrder.Checked += new RoutedEventHandler( RushOrder_Changed );
RushOrder.Unchecked += new RoutedEventHandler(RushOrder_Changed);

Be careful here, Intellisense will want to name the methods for Checked and Unchecked
RushOrder_Checked and RushOrder_Unchecked respectively, but there is no need for two
methods. We'll override that by typing in the name we want, RushOrder_Changed, which will
cause both events to use a shared event handler.

In the shared event handler, we'll check the IsChecked status of the checkbox and if it is checked,
we'll change its text to all upper case (just so we know the event handler is working).

void RushOrder_Changed(object sender, RoutedEventArgs e)


{
if (RushOrder.IsChecked == true)
{
RushOrder.Content = "RUSH";
}
else
{
RushOrder.Content = "Rush";
}
}

Note to C# programmers, we must write


if (RushOrder.IsChecked == true)

and not

if (RushOrder.IsChecked)

because IsChecked is of the new C# 3.0 type nullable bollean ( bool?) which indicates that it
may have three states, not two: true, false or null.

Creating Controls Dynamically


In Silverlight 2, all Xaml controls are isomorphic with CLR objects. That is, anything you can
create in Xaml you can create in code.

Where you might write,

<Button x:Name="myButton" Content="Hello" />

You can also write

Button myButton = new Button();


myButton.Content = "Hello";

While it is possible to create all your controls and objects in code, best practices dictate that it is
usually better to do so in Xaml. The most compelling reason is that Xaml is highly "toolable" -
that is, it lends itself to round-trip modification in tools such as Visual Studio and Expression and
thus is easier to scale, modify and maintain.

On the other hand, there are times that you can't know at design time which or how many objects
you'll need, and the ability to create objects dynamically can be a fundamental requirement.

We can make a minor modification to our existing program to add another button on the user's
request. Add the following button to the Xaml file (note that we also name the canvas!)

<Canvas x:Name="myCanvas">
<Button x:Name="myButton" Content="Hello"
Canvas.Left="10" Canvas.Top="10"/>
<Button x:Name="Another" Content="Add Another"
Canvas.Left="10" Canvas.Top="50" />
<CheckBox x:Name="rushOrder" Content="Rush"
Canvas.Left="50" Canvas.Top="10" />

The effect is to add a button to the page with the words "Add Another." When the user clicks on
this button, we want to add a button to the UI and we want that button to have its own size,
position and behavior.
We do all of that in the code behind.

Creating a Button Dynamically

Save the .xaml file and open Page.xaml.cs

The first thing to do is to add an event handler for the new button,

Another.Click += new RoutedEventHandler(Another_Click);

And to add its implementation, in which you'll create a new button and set its properties,

void Another_Click(object sender, RoutedEventArgs e)


{
Button b = new Button();
b.Content = "I live!";
b.SetValue(Canvas.LeftProperty, 10.0);
b.SetValue(
Canvas.TopProperty,
this.newButtonPosition);
this.newButtonPosition += 30.0;
b.Width = 100;
b.Height = 20;
b.Click +=new RoutedEventHandler(new_button_click);
myCanvas.Children.Add(b);
}

Because the canvas's Left and Top are not actually properties of the Button (but are extended
properties) you set them with the SetValue method, which takes two parameters. The first is the
actual name of the property you want to set (Canvas.LeftProperty and Canvas.TopProperty)
which is not difficult to find as Intellisense will supply it) and the second is the value (in this
case a double).

Because I want to be able to click this button more than once, and I don't want the new buttons
overwriting each other, I need a member variable to keep track of the top value,

private double newButtonPosition = 100.0;

After I set it, I can then increment it so the next button will fall below the previous,

b.SetValue(
Canvas.TopProperty,
this.newButtonPosition);
this.newButtonPosition += 30.0;

The result is very satisfying,


Figure 1-14. Adding Dynamic Buttons

The second thing to notice about the Button we are adding is that we are also adding an event
handler registration,

b.Click +=new RoutedEventHandler(new_button_click);

This means that every button that is added (each of the three shown in Figure 1-8) is registered to
call new_button_click when it is clicked. You'll need to write that event handler into your
Page.xaml.cs file,

void new_button_click(object sender, RoutedEventArgs e)


{
Button btn = sender as Button;
btn.Content = "Don't do that!";
btn.IsEnabled = false;
}

As the buttons are clicked, they change their message and become disabled,

Figure 1-15. Disabled Dynamic Buttons

Finally, none of this will work if you don't add the buttons to the page. In this case, however, you
don't want to add them to LayoutRoot as their positioning is in terms of a specific canvas, so
you'll add them to that canvas,

myCanvas.Children.Add(b);
When you run the program, they are members of that canvas, just as surely as if you had added
them individually in the Xaml file.

Você também pode gostar