Sunday, July 8, 2012

Metrocizing XAML - Part 1: Data Templates

Let's face it: gradients and glassy buttons just aren't "cool" anymore.  In a way, it sucks; these applications aren't that old (just a year or two).  But now they look dated.  And this is what makes XAML awesome.  Since XAML is "lookless" (meaning that the visual representation of the controls is separated from the underlying operation), we can rip out an old look and drop in a new one without needing to change our application code.  And if we have proper separation of the thematic parts of our application, those updates can be isolated to a single location.

As I mentioned a couple weeks ago, I went through several of my sample projects and "metrocized" them.  Since these are XAML solutions (several WPF and one Silverlight), the updates were not complicated.  Let's take a look at an "old" and a "new" screen together.  These are taken from the IEnumerable, ISaveable, IDontGetIt: Understanding .NET Interfaces samples (specifically the IEnumerable.sln).

Old Layout

New Layout

These projects are available for download in a combined solution here: Jeremy Bytes - Downloads.  The "Old.UI" project contains the old layout; "New.UI" contains the new layout.

XAML is Awesome
The best part of this whole process is that XAML completely separates the visual display from the controls themselves.  This gives us the chance to change the way our application looks without having to change the underlying code.  And as we go through this example, we'll see just that.  We didn't need to change any of the application code. (Note: There is one small change to the Value Converter code; but this was done to put some different colors into the converter.)  The rest of the updates are in the XAML itself.

One thing to note: I am not a UX designer.  I put together passable user interfaces that are pleasing and functional, but I'm not one of those UI wizards (you know who I'm talking about -- the guys that come up with incredible designs, and you smack yourself on the forehead: "That's so obviously awesome!").  I put together the bulk of the design updates in about half a day (colors and layout).  It took me a little longer to iron out some of the kinks in the Control Templates.  Once the hard part was done, implementing the changes in the different application was very easy (mostly just replacing XAML in the right places).

We'll be looking at these updates in 2 parts.  The first part (this one) will cover the updates to the ListBox -- the one with the Person objects listed.  This is primarily concerned with the Data Template used by the ListBox, the Value Converter for the color, and a few other minor updates.

The second part (next time) will cover the updates to the Buttons.  Our original application used the standard button look-and-feel.  The new application uses a custom control template.  With the control template, we control the layout, the design (such as the arrows), and also the display behavior -- although you can't see it in the screenshot, the buttons have different colors when you hover over them or click them.  This template is fairly simple (compared to how far you can go with control templates), but it has the effect that I was looking for here.  We'll walk through this sample in Part 2.

ListBox Updates
The ListBox itself stays pretty much intact.  The primary differences are the placement in the application Grid (on the right instead of the left), and the inclusion of a WrapPanel -- this gives us the ability to show multiple columns in our ListBox.  Let's compare our old and new markup to see the changes.

First the old markup (from MainWindow.xaml in Old.UI):

Now the new markup (from MainWindow.xaml in New.UI):

The primary difference is the addition of a WrapPanel.  We did this by adding tags for the ListBox.ItemsPanel and the ItemsPanelTemplate.  By using the WrapPanel, we are specifying that if we run out of space, to "wrap" the list to another column.  The WrapPanel has an Orientation property to determine whether to wrap vertically or horizontally.  "Horizonal" is the default, and so that is the direction we have here.

Normally, a ListBox would just scroll in order to accommodate any items that don't fit on the screen (either horizontally or vertically).  Because we have our wrap panel going horizontally, we need to disable to built-in horizontal scrolling of the ListBox (otherwise, it won't actually "wrap" to the next row).  This is why we added the "ScrollViewer.HorizontalScrollBarVisibility="Disabled"" attribute: to disable horizontal scrolling.

The items in the screenshots are in the same order: John Koenig, Dylan Hunt, John Crichton, Dave Lister, John Sheridan, Dante Montana, Isaac Gampu.  If we look at the new sample, we see that Dylan Hunt (the 2nd item) comes horizontally after John Koenig.  Then we "wrap" to the next line for the 3rd and 4th items).

If we wanted to wrap vertically (so the items go down the first column, then down the second column), we would simply set the WrapPanel Orientation to Vertical, and then disable the Vertical scrollbar on the ListBox.

A Note About the WrapPanel
The WrapPanel is a standard control in WPF 4 (Visual Studio 2010).  If you are using Silverlight (4 or 5), the WrapPanel is available as a separate download as part of the Silverlight Toolkit.  I used this same ListBox layout in a Silverlight 5 application, and it worked just the same as the WPF version.

The ListBox Data Template
So, the updates to the ListBox itself are not very extensive, but the items are displayed completely differently.  This is because we are using a separate Data Template to control the layout.  This is denoted in our markup by the ItemSource = {StaticResource PersonListTemplate}" attribute.

If you are not familiar with Data Templates, I would highly recommend that you take a look at Introduction to Data Templates and Value Converters in Silverlight (this works the same in WPF).  This covers the creation of the Data Template that is used in the "old" application.  We'll just be looking at the differences here.

The Old Data Template
First, let's review the old Data Template.  This is located in the Resources section of MainWindow.xaml (in Old.UI):

Just a few notes here: first we have a Border that surrounds our entire template.  The BorderBrush is databound to the StartDate property of the Person object.  This goes through a Value Converter (myDecadeBrushConverter) to turn the date into a brush.  The result is the border color of each item is determined by the decade of the StartDate property (different colors for 1970s, 1980s, 1990s, and 2000s).

The rest of the Data Template is described in the article mentioned above.  Basically, we have a collection of StackPanels and TextBlocks to layout the data in the format that we want.

Here are the results for Dylan Hunt:

The New Data Template
The new Data Template is a bit more complex.  The first thing to note is that it is no longer in the MainPage.xaml file.  The Data Template (along with all of our other resources) have been moved to App.xaml.  The App.xaml Resources section gives us a place to put resources that are available to our entire application.  In this sample, we only have one screen, so we don't get much from sharing.  But we do get a big benefit from centralizing all of our "theming".  If we want to change the look in the future, we only have this one place to look (rather than in the separate XAML files).  Managing Resources is a bigger topic with lots of options (such as creating completely separate resource dictionaries and assemblies).  If you're building larger applications or a suite of applications that all share the same "look", then you'll want to check into this further.

The first part of the new Data Template contains the Border element (from App.xaml in New.UI):

The big change here is that we are no longer binding to the BorderBrush property, we are binding to the Background property.  This gives us our different color backgrounds for each item.  The Value Converter has also been updated a bit (for the new colors and a bit of optimization).  We'll take a look at this in just a bit.

Next, since our layout is a bit more complex, we have a Grid to help us layout our controls:

This gives us 3 rows and 2 columns to work with.  Then we have the main layout of our controls:

Again, nothing too unusual here: we're just using StackPanels and TextBlocks like we did before, just with a different layout.

The Styles for the items have been broken out (also in App.xaml of New.UI):

This lets us control the Font properties and alignment separately.  If we change our minds about these settings, we can just update the Styles, and our controls will pick them up automatically.  Note here that we are using "White" text since we have a contrasting background color.

Here's the result for Dylan Hunt:

Updates to the Value Converter
We also made some updates to the DecadeBrushConverter.  This converter returns a Brush object based on the decade of a DateTime property.  Our old converter controlled the Border color; the new converter controls the Background color.

Let's look at our original converter (in Converters.cs in Old.UI):

The original Value Converter used a series of "if" statements to determine whether the DateTime value was part of a particular decade.  If so, then it returned a SolidColorBrush with an appropriate color.

The new converter has been refactored just a little bit.  Here's the new code (in Converters.cs in New.UI):

The biggest functional difference is in the colors that are returned.  Rather than being the primary colors of the old border, we have selected more "Metroid" colors for the background.

The change from the series of "if" statements to a "switch" statement was made due to cyclomatic complexity.  If you have Visual Studio 2010 Ultimate, you can calculate the Code Metrics for a project (under the "Analyze" menu).  One of the items is the Cyclomatic Complexity.  This value is like golf scores: smaller is better.  The cyclomatic complexity basically tells how many different possible code paths exist in the code.

With the series of "if" statement, the cyclomatic complexity was increased because we have 2 conditions in each "if" (the year is greater than or equal to one value and less than another value).  In our refactored code, we calculate the decade before running through the decision process (by doing integer math, when we divide by 10 we lose the last digit; when we multiply by 10 we add a "0" back on -- this gives us the decade).  Since we have the decade already, we can use a "switch" statement which only has 1 condition for each item (instead of 2).  This reduces our cyclomatic complexity even though our ultimate output is exactly the same.

As a bigger benefit, I think the second version is easier to read (but that's just a personal preference).

Changing the Look with XAML
So, we went from this:

to this:

without changing any of our application code.  All we had to do was update our XAML and our Value Converter.  That's pretty cool.

Next Time
Today, we looked at how we can update XAML Data Templates to give us a completely different look to our ListBox -- without changing the application code at all.

Next time, we'll look at how I created the control templates that are used for the buttons.  The old solution just used the default button templates, but we'll be able to see the changes from a combination of border, text, and button to a single custom-templated button that matches our Metro-ish style.

Happy Coding!

No comments:

Post a Comment