Sunday, July 8, 2012

Metrocizing XAML: Part 2: Control Templates

In Part 1, we saw how we could update the look and feel of our XAML applications by changing a bit of layout and our Data Template.  This time, we will take things a step further.  First, we'll do a quick overview of some of the minor changes (colors and general layout), and then we'll dive into creating a custom control template for our buttons to make them more Metro-ish (Metroid?).  As a reminder, here are the UIs of our "old" and "new" applications:

Old Layout

New Layout

The source code for both of these projects is available here: Jeremy Bytes - Downloads.  These applications are in the "Old.UI" and "New.UI" projects respectively.

Application-Level Updates
Several of the updates to the application have to do with the general layout and colors.  You can check the XAML for more details on this.  The key features are the re-arrangement of the grid (swapping the ListBox and Button panels), removal of the background gradient, and the insertion of a solid background.

As mentioned in Part 1, the resources for the application (brushes, data templates, and value converters) were moved from MainWindow.xaml to the App.xaml Resources section.  Let's start by looking at the top of our App.xaml (from New.UI):

The first thing to note is that we have moved our Value Converters from MainWindow.xaml to here.  This was necessary because several of the value converters are used in the ListBox Data Template that is also in this file.  To bring in the Value Converters, we needed to add the namespace for the local project (so we can access the classes in Converters.cs).  For more information on Value Converters and how they get added, please see Introduction to Data Templates and Value Converters in Silverlight (also works in WPF).

The next section contains brushes for our application.  There were no resources for these items in the old application.  I added them here so that it would be easy to update the application colors in the future (since Metro-ish applications will go out of style sometime in the future).  Notice that I named the resources after what they are used for and not what colors they are.  If I had named the resource something like "LightGrayBrush", then it would be difficult if we wanted to change it to another color.  Since the name describes what the brush is used for (rather than what it looks like), we can change this to blue in the future without worrying about mis-matched names.

These application brushes are tied to the XAML in MainWindow.xaml (such as setting the application background).  And, as we'll see, these are also used in our control templates for the buttons.

Let's see what else is in the App.xaml (sections are collapsed to get the overview):

First, we have a Control Template and Style for our "GoButton".  We'll be spending quite a bit of time in this detail below.  Next, we have a Control Template and Style for our "ClearButton".  The buttons differ in the icons (an arrow vs. an X), but they otherwise behave the same.

Next we have 3 different TextBlock styles for "ApplicationText", "ListItemTextStyle", and "ListTextStyle".  We saw the ListItemTextStyle and ListTextStyle in use in our Data Template in Part 1.  Finally, we have our Data Template for the ListBox.  We looked at this in detail in Part 1.

Comparing Buttons
So, what are the differences between our old and new buttons.  Let's compare them side-by-side:

Old Button

New Button

One big difference is operation.  The old button is only clickable on the part that looks like a button (the "Fetch" part).  The new button is clickable anywhere in the rectangle.  This makes it much friendlier to touch-enabled applications since it is a much bigger target.

Let's compare the XAML, starting with the old button in MainWindow.xaml (in Old.UI):

Notice that we have a border that encloses a StackPanel.  And that StackPanel contains a TextBlock and an actual Button.

Compare this to our new XAML (in MainWindow.xaml in New.UI):


The difference is here we just have a Button with the content of "Concrete Class".  So, where's the rest of it (the border and the icon)?  That's all part of the custom Control Template.  And it's getting applied to this button through the Style property.

The Button Style
As we saw earlier, the GoButtonStyle is in the App.xaml.  Now, let's take a look at the details:


The Style allows us to apply settings to properties centrally.  We have 2 buttons in our application that use this Style (the "Concrete Class" button and the "Interface" button).  But we only have to set these properties once.  And if we decide that we want to change something (like the FontSize), we just update it here, and it automatically propagates to all buttons that are using this Style.

Notice that the "Foreground" and "Background" properties are set to our "Application" brush resources that we set above.  This is important.  We want our buttons to have the same background as the application.  If the application background changes, we want our button background to change along with it.  (Note: this might not always be the case, but that's the behavior that we want here.)

Finally, we have the "Template" property set to the Control Template that's also in our App.xaml.

The Button Control Template
Before we look at the specifics of the GoButton Template, I want to say a few words about Control Templates.

As mentioned in Part 1, XAML controls are "lookless".  This means that the behavior is completely separated from the visual display.  We are provided with default templates (so that we don't have to create our own), but the templates are fully customizable and/or replaceable.

Control Templates are generally very complex.  They are designed to handle a variety of states.  For example, a button has a number of states, including "Normal", "MouseOver", "Pressed", "Disabled" as well as "Focused" and "Unfocused" (in addition to others).  If you want to have all of these features available, then it's often easiest to use Expression Blend to export the current control template for you to modify.

In our case, we are only handling a subset of these states (Normal, MouseOver, and Pressed), and we don't worry about the other states (since they aren't really applicable to our application).  One thing to note: just because we do not implement a visual change for a State does not mean that that State does not exist. For example, our control template does not implement "Disabled", but our button can still be disabled -- it just won't look any different from an enabled button.  Same with "Focused" -- the button itself still supports the idea of "focus", but it will not look any different if it is focused.

If we were creating a set of custom templates to be used more extensively, then we would definitely want to implement all of these states.  As it is, we'll just focus on the ones we care about for this application.

Control Template Overview
Our Control Template is more complex than other bits of XAML that we've seen so far, so we'll break this down into several different pieces.  First, let's look at an overview of the Control Template with several of the areas collapsed:


First, we have the ControlTemplate tag.  The TargetType lets us know what kind of control this applies to.  This template can only apply to Buttons.  If we were to try to apply it to a TextBox or ListBox, we would get an error.  The Key let's us reference this like any other Resource.

Our outer element is a Grid.  This is there primarily to hold the other elements; we don't have any Rows or Columns defined at this level.

Inside the Grid is the VisualStateManager.  This is how we provide different looks for the States that we mentioned above.

The Border is the first visual element.  We'll take a closer look at the details of this in just a moment.  Inside the Border, we have Grid for layout purposes.  This Grid has both Rows and Columns and contains our ContentPresenter and our Canvas.  We'll see more details on these in just moment as well.

Now, let's go through each of these parts.  We'll start with the primary elements and then swing back up to the VisualStateManager at the end.

The Button Border
Here is the complete markup for the Border (the outer edge of our Button):


Notice that the BorderBrush property is set to a "TemplateBinding".  The TemplateBinding markup extension indicates that this should be bound to one of the main properties of the Button.  In this case, we want the BorderBrush to be the same as the Button's "Foreground" property.  And remember from our Style, the "Foreground" is set to the "ApplicationTextForeground" by default.  But this can be changed.  If we change the Button's Foreground property (either in the property inspector or in the markup, then the BorderBrush will change along with it.

The Border Background property is a little bit different.  Notice that we have a SolidColorBrush that is set to the TemplateBinding of Background.  This let the background color change if the Button Background property is updated.  Notice also that we have a x:Name on our brush (ButtonBackgroundBrush).  We gave this element a name because we will use it in our VisualStateManager -- when the State changes, we want the Background to change.  We'll come back to this.

The Button Main Layout Grid
The next element is the Grid which has our main layout for the Button:


Let's look at our button again:


The Grid defines where we will place our elements.  The first Grid Row/Column contains the "ContentPresenter".  The ContentPresenter is responsible for displaying whatever is in the "Content" property in the Button.  In our case, the Content is simply text ("Concrete Class").  We use a ColumnSpan of 2 so that the content can stretch the full width of the Button.

Notice that our ContentPresenter does not have any sort of Font information included.  Any text that appears in the ContentPresenter automatically picks up the Font information from the Button itself.  Since we have all of that information set on the Button Style, we don't need to worry about it here.

As a side note, if we tried to put more than just text into the Content property, our Button would probably behave strangely.  This is another area that you need to look into further if you are interested in creating your own button templates that can be used in a variety of situations.

The Canvas is in the second Grid Row/Column and contains our arrow.  Since the Row/Column definitions are set to "Auto", this will only be as big as the contents.  Since the first column is set to "*", it will take the remaining space.  The result is that our icon will be aligned to the bottom right side of our Button.

The Arrow Icon
For the Arrow Icon, we could have used a graphic, but that's generally not the best approach.  XAML is designed so that things can be easily resized, stretched, or re-flowed to fill in available space.  The best way to make sure that your controls can handle stretching/resizing is to use vector descriptions rather than a .gif or .jpg.

I shamelessly stole this arrow from Laurent Bugnion's blog: 56 Vector Arrows in XAML (isn't the Internet great?).  This blog article includes a bunch of different arrows (56) that are all described in XAML paths.  I found one that I liked, did a little cutting, and pasted it into my application (then tweaked the colors a bit to fit the style).

I won't show the entire output (since a lot of it is a list of numbers for a Path), but here's the relevant bits:


For the Path statements that make up the arrow and the circle, I made a couple of changes.  First, I set the "Fill" property to a TemplateBinding to match the Foreground of the Button.  Then, I changed the Opacity to "0.5".  This will make the arrow semi-transparent (with the effect of a lighter color).  This means that the arrow icon will look lighter than the Button text even though they are the same color.

The "Data" property contains the meat of the path, specifying all of the points in the arrow and circle.

The Visual State Manager
Now that we've see all of the default visual elements, it's time to look at the VisualStateManager.  This determines what our Button will do when the various states change.  Again, what we have here is very simple; we could make this much more interesting/complex very easily.


Our VisualStateManager has a number of VisualStateGroups.  The States that we care about are all in the "CommonStates" group, but the Button also has "FocusStates" and "ValidationStates".  The reason there are different groups is to allow for overlap.  For example, we could have a button that is both "MouseOver" (from the CommonStates) and "Focused" (from the FocusStates).

Since we only care about the CommonStates, we just have one VisualStateGroup in our Control Template.  Inside the group, we have 3 different VisualStates.  Each VisualState determines what will happen when the control enters that state.  In each of our VisualStates, we have defined a Storyboard with an Animation of Duration zero.  This means that we are animating a property (the background color), but the zero duration means we have no "transition" -- the color change happens immediately.  If we wanted to be more creative with our transitions, we could add additional animation (such as pulsing when the control is focused).

Our three VisualStates all set the same property.  Notice that the Storyboard.Target is set to the ButtonBackgroundBrush.  This is the name of the brush in our Border.Background that we saw above.  Then we have the Storyboard.TargetProperty which specifies what property we want to set.  In this case, we want to set the "Color" of the brush.  Finally, we have the "To" that specifies the new value for the property.

For the "Normal" state, we set the color to the "TemplateBinding Background" (which is the default background color we want).  For the "MouseOver" state, we set the color to LightSlateGray, and for the "Pressed" state, we set the color to "White".  (Note: we would probably want to set these to resource colors or TemplateBindings for a more robust Control Template.)

When we put all of these elements together, we get the visual layout and state-change behavior for our GoButton.  The template for the ClearButton is very similar.  The primary difference is that instead of using a set of Paths for the icon, it simply uses a large letter "X".  Again, for a production application, we would probably want to take a little more time to do a vector graphic.  But this works for our simple case.

Putting It All Together
So, now that we've looked at the updated Data Template, Value Converter, Application Brushes, Styles, and Button Control Templates, we can see how these pieces all fit together to form the fresh look of our application:


And remember, we did all of these changes without modifying our Application code -- the application behaves just like it did before (loading in data from a web service and displaying it in a ListBox).  And now we have a completely new look with about a day's worth of effort (and a lot of that was experimenting with colors and layout).

XAML is pretty awesome, huh?

Happy Coding!

No comments:

Post a Comment