Saturday, June 13, 2009

More Silverlight Part 2 - User Controls & Events

The story so far: In More Silverlight Part 1, we created a Silverlight 2 application and a WCF service. We then used a DataTemplate and various ValueConverters to format and display the data in the ListBox.

Here's what the app looked like when we left off:

(Click image to enlarge)

Now, we're going to move on, first by creating a Detail panel, then abstracting out the ListBox to a User Control, and finally creating an Event that will let us know when a new Person record is selected.

Creating a Detail Panel

We'll start by adding a Detail panel that shows the First Name, Last Name, Birth Date, and Age for a single Person (the Person selected in the ListBox). This will be a StackPanel that is in the 2nd column of the Grid in our Page.xaml.

(Click image to enlarge)

The code is pretty self-explanatory. We set up TextBlocks to serve as labels for the data, and TextBlocks with data binding to hold the data. You'll notice that we are using the same DateConverter and AgeConverter that we used in the ListBox.

Getting Data into the Detail Panel

We want the Detail Panel to update whenever the selection changes in the ListBox. To do this, we'll hook up a "SelectionChanged" event to the ListBox. As a reminder, you can just type "SelectionChanged=" and Visual Studio will give you the option to add a New Event Handler.

(Click image to enlarge)

Now, we'll navigate to the Event Handler in the Page.cs file. What we need to do here is set the DataContext of the DetailPanel. But the question is, what should we bind to? Do we need to make a service call?

The answer comes through the fact that Silverlight data binding is extremely flexible. We can bind to any object, including an object that exists somewhere else on the page. In this case, we want to bind to the Person that is selected in the ListBox. So, we do just that.

(Click image to enlarge)

A couple of things to note here. First, you see that we are getting the SelectedItem for the ListBox and then casting it as a Person. What you should be aware of is that this may result in a null value. The SelectionChanged event will fire whenever the currently selected item changes; this includes when an item is un-selected. When a ListBox is first loaded, the default is to have no selection. This event will fire, and the SelectedItem will be null.

In many circumstances, you will want to check the "var p" to see if it is null after the assignment. In our case, we will want to let p be null (we'll see why in just a bit).

After we get the SelectedItem cast as a Person, we just assign this as the DataContext of our DetailPanel. Pretty straightforward, huh? Here's the result:

(Click image to enlarge)

To see how the nulls are handled in the Event, simply click the Button again. This will clear the selected item in the ListBox, which will in turn clear out the DetailPanel data.

Creating a User Control

But what if we want to re-use the Person ListBox multiple places in our application? Rather than duplicating code, it would be better to abstract out this piece as a separate User Control. That's what we'll do right now.

Step 1: Add a new User Control to the Silverlight project
Right-click on the Silverlight project, select "Add", then "New Item". From the dialog, select "Silverlight User Control" and name it "PersonListControl". This will create a default PersonListControl.xaml and PersonListControl.cs that look very similar to what we started with for Page.xaml/Page.cs. This is because Page is also a Silverlight User Control.

Step 2: Cut the StackPanel xaml from Page.xaml to PersonListControl.xaml
We'll be moving everything from the first Grid column of Page.xaml to the new user control. Just cut the entire StackPanel (the one that has Grid.Column="0"), and paste it over the Grid (LayoutRoot) in PersonListControl.xaml. This will leave us with a StackPanel as the root element of the user control. Note: after pasting it in, you will want to remove the "Grid.Column" attribute. The app will still run with it there, but it's better to keep the xaml as clean as possible.

Step 3: Copy the UserControl.Resources Section
The xaml that we moved in the step above makes use of the Converters we created earlier. This means that our new user control needs to have references to these as well. The easiest way is just to copy these from the Page.xaml. Note that we are doing a copy (not a cut) because these are used in the DetailPanel that is staying on Page.xaml.

Copy the "xmlns:local=" namespace and the entire "UserControl.Resources" sections with all three converters to PersonListControl.xaml.

Step 4: Change the Size
Our new UserControl will need to fit in the first column of our main Grid, so we'll want to change the sizes a bit. Change Width to 240 (to allow for a little bit of padding) and the Height to 300.

Step 5: Move the Event Handlers
The xaml that we moved contained 2 Event Handlers: one for the ListBox selection changed event, and one for the Button click event. Now we'll move the Event Handlers as well.

From the Page.cs file, cut the "PersonList_SelectionChanged" and "GetPeopleButton_Click" methods and paste them into PersonListControl.cs. One thing to note: since the PersonList_SelectionChanged event directly references the DetailPanel, this won't compile. For now, just comment out the 2 lines in this Event Handler. We'll be re-implementing this a little later.

You should be able to build now, but if you run the application, you will find that we still need to place the new user control onto our main Page.

Step 6: Using the User Control
Actually using the new user control on our main Page couldn't be easier. In the spot in our Page.xaml where we used to have a StackPanel (in the first Grid column), we will now have our control:

(Click image to enlarge)

Notice that this uses the "local" prefix (the same as our Converters) since everything is in the same namespace. If we were to split out the User Control into a separate namespace, then we would need to add a new xmlns and give it a different alias to use.

We can now run the application, and the ListBox will look just like it did before. If we click the Button, it will fetch the Person list and display it. The application doesn't work completely, though. We can't get the data into the DetailPanel. For that, we will need to expose an Event in our new User Control.

High Level Event Overview

Before we create the Event, let's take a look at how we'd like it to work. What we want to do is get the Person that is selected in the ListBox and display it in the DetailPanel. The problem is that the ListBox and DetailPanel are now in different scopes. So, we need to get the Person from the ListBox to the DetailPanel somehow. We'll do this by exposing an Event in the ListBox that has the selected Person as part of the EventArgs.

We'll need a custom EventArgs class that will hold the selected Person. Then we'll set up an Event on our new User Control that we can hook in to from our Page.

Custom EventArgs

We'll create the custom EventArgs class in the PersonListControl.cs file. You may want to abstract this out into its own file for other projects, but we won't do that here. We'll descend from RoutedEventArgs (which is a little more specific than EventArgs and is commonly used in Silverlight and WPF). Our custom EventArgs only needs to have a single Property (SelectedPerson) and a contructor that takes a Person as a parameter. Here's the completed code:

(Click image to enlarge)

Declaring the Event Handler

Next, we need to declare the Event in our PersonListControl class. We can do this with a single line of code:

(Click image to enlarge)

Note that there are several ways to make this declaration, including using "delegate" syntax. The syntax used here is the most compact. You'll see that we are using our custom EventArgs class as a generic type. Our Event is called "SelectedPersonChanged".

Invoking the Event Handler

To invoke the Event Handler, the convention is to create a method called OnEventName -- in our case "OnSelectedPersonChanged". Take a look at the code below:

(Click image to enlarge)

If the comments look familiar, it's because they came straight from the Microsoft Help file. They aren't really applicable for this particular application because we're not making descendant classes or doing complex invokation, but it's good to use the "safe" version so that we can get used to it.

You'll see that we are using our custom EventArgs as a parameter for this method. And as noted in the comments, we are making a copy of the handler first. This is because we can only invoke handlers that are actually hooked up to something (i.e. not null). If the handler is null, then we do not want to do anything (and thus we have the null check). The reason that we make the copy is that it is possible that the handler is deleted between the time we check for null and the time we invoke the method. If we make a copy, then we are acting on a snapshot in time, and so eliminate this possibility.

Raising the Event

Now we need to actually raise the event. We will do this in the "PersonList_SelectionChanged" method (the one that we commented out earlier). We will get the SelectedItem from the ListBox and cast it to a Person -- just like the old ListBox SelectionChanged handler. Then we'll use that Person to create the custom EventArgs and pass it to the OnSelectedPersonChanged method.

(Click image to enlarge)

Notice that it is possible for the Person to be null (as we discussed above). This is okay. If there is no SelectedItem, then we will pass the null through the Event and ultimately to the DetailPanel (as we'll see next). Be sure to build everything to make sure that there are now errors. If you run, you still won't see any difference. Yet.

Using the Newly Exposed Event

Now we'll go back to our Page.xaml to hook up the new Event. You'll notice that if you start typing "Select" in the PersonListControl element, IntelliSense will show the Event. Like other events, if you type "SelectedPersonChanged=", Visual Studio will let pop up "New Event Handler" (as we saw above). If you do this, you'll end up with something like this:

(Click image to enlarge)

Finally, we'll navigate to the event handler in the Page.cs file. Since the custom EventArgs exposes a SelectedPerson object, we just need to assign this to the DataContext of the DetailPanel.

(Click image to enlarge)

If you run the application now, you'll see a fully functioning Person ListBox and DetailPanel.

(Click image to enlarge)

This will behave the same as above. If you click the Button again, the DetailPanel is cleared. This is because the SelectedItem is cleared when the ListBox is loaded. Because the SelectedItem changed, the Event fires. And because there is no SelectedItem, the EventArgs contains a null SelectedPerson. This results in a null for the DataContext for the DetailPanel (which is what we want here). You may need to rework this if you need different behavior.

We've taken our application and abstracted out a significant part of it into a separate User Control. Then we implemented an Event on that User Control that we can use when we add the element to our Page. If we were to have several different forms that needed this same Person ListBox, we can simply add the custom User Control and get all the same functionality. We even have an Event to hook into so we know when the selected Person changes. These are just the basics, of course. For a production app, we would want to add Error handling and other such things.

As you can see, Silverlight has several powerful features that allow us to create flexible and well-designed applications. Since pretty much everything in Silverlight is a User Control, it's possible to nest these elements within each other and come up with some interesting abstractions in our UI.

Happy Coding!

Saturday, June 6, 2009

More Silverlight Part 1 - Data Templates & Value Converters

I've pretty much finished up my Silverlight 2 project at work, so I thought I'd share a few more useful things that I've come across. This time, we'll take a look at Data Templates (which make List Boxes extremely flexible) and Value Converters.

Setting Up the App

We're going to start by building a application similar to what we built in the last few posts: a Silverlight 2 application that gets data from a WCF service. Start by following Steps 1 - 3 from this post. I'll point out where we'll make a couple of changes.

Step 1: Create a New Silverlight Application
We'll call it "TemplatesConvertersEvents" this time. Let it create the ASP.NET hosting site as well.

Step 2: Build a WCF Service

Step 2a: The WCF Service Contract
We'll create the same PersonService, but we'll add a new field "BirthDate" to the Person class. See the Service Contract below:

(Click image to enlarge)

Step 2b: The Service Implementation
Again, this will look pretty similar. We'll just add a BirthDate value to each of the Person objects.

(Click image to enlarge)

Step 2c: WCF Service Configuration
Same as last time. Make sure to change the binding from "wsHttpBinding" to "basicHttpBinding".

Step 3: Add the WCF Service Reference to the Silverlight Application

Step 4: Update the XAML.
This is pretty similar to the previous examples, with just a few differences. Check out the code sample below for Page.xaml. Then we'll walk through it.

(Click image to enlarge)

Start by changing the Width and Height properties of the page (500 x 300). Then replace the default Grid with a Border. The Border contains a 2 column grid. We'll be concentrating on the first column here (the 2nd column will be used in Part 2). The Grid contains a ListBox.

Notice that the ListBox looks a little different. We are no longer using the "DisplayMemberPath". Instead, we have an ItemTemplate that contains a DataTemplate. The DataTemplate is simply a container that can hold whatever content you like. For now, we'll just include a TextBlock that is bound to the FirstName field. This will have the same effect as if we were to set the DisplayMemberPath. One thing to be aware of: the DisplayMemberPath and ItemTemplate are mutually exclusive; if you try to include both, you will get an error.

Finally, we have a Button that looks similar to our previous examples. As a reminder, to add the Click handler, just type "Click=" and Visual Studio will pop up an option for "New Event Handler". This will create a handler based on the control name and put the stub in the code-behind.

Next we implement the handler. You can right-click on "GetPeopleButton_Click" and choose "Navigate to Event Handler" and it will take you to appropriate spot in Page.cs. The handler implementation should look like this (again similar to what we've seen before):

(Click image to enlarge)

Another reminder: you will need to add the "using" statement for the PersonService. If you type in the code above, you can place the cursor on "PersonServiceClient", then press Ctrl+. to bring up the option to add the "using".

You should be able to build and run the application now. After you click the button, the results should look like this:

(Click image to enlarge)

Expanding the Data Template
Now we'll see the power that the ListBox has by expanding the DataTemplate a bit. The DataTemplate can only have a single child element, so if we add multiple items, we'll need to wrap them in some sort of container. We'll use a set of nested StackPanels -- some vertical and some horizontal.

(Click image to enlarge)

This is fairly straightforward XAML. Note that we have multiple TextBlocks that are databound and a little bit of formatting. Here's what we get when we run the app now.

(Click image to enlarge)

Creating a Value Converter
One thing I don't like about the output is the format of the BirthDate field. To take care of this, we'll use a value converter. A value converter is simply a class that implements the IValueConverter interface (profound, eh?). The interface consists of 2 methods: Convert and ConvertBack. An incoming value is passed in as a parameter; all you need to do is do the conversion and return the new value.

Our DateConverter isn't going to be that interesting, but it's a good place to start (then we'll look at something a little more challenging).

In the Silverlight project, add a new Class. I've called mine "Converters.cs" because I'll use the same file to hold all of my converters. Then we'll rename the class from "Converter" to "DateConverter" and add the IValueConverter interface. IValueConverter is in the System.Windows.Data namespace, so you'll need to add this to the "using" block as well.

As we've done before, we'll just right-click on "IValueConverter", choose "Implement Interface", then "Implement Interface" again. This will put the "Convert" and "ConvertBack" method stubs into our class. We'll only be doing one-way conversion (read-only data), so we will only implement "Convert". If you had a TextBox or other editable control, then you would also implement "ConvertBack" which reverses the process. Here's our code:

(Click image to enlarge)

Since the "value" parameter is of type object, we'll need to start off by casting it to the inbound type that we are expecting (DateTime in this case). Our outbound type is simply a string, so we'll call the ToString function with a formatting string that we like. Note that there are additional options (some that are especially useful for Date conversions) by using the "parameter" and "culture" parameters. Take a look at the Microsoft Help file to get more information on those.

Using a Value Converter
So, now that we have a DateConverter, how do we use it? First, we'll need to add the namespace to our xaml (see below). Again, VisualStudio is helpful here; if you type "xmlsns:local=", then you will get a list of namespaces that are in the project, including everything that appears in the project References. We'll select the project namespace "TemplatesConvertersEvents". You can use whatever alias you like for the namespace; "local" is a convention for items contained within your project.

The next step is to add our DateConverter as a Static Resource.

(Click image to enlarge)

Now all that's left is to add the Converter to the Binding declaration in our TextBlock.

(Click image to enlarge)

If you run the application now, you'll see that we have a nicely formatted mm/dd/yyyy date for the BirthDate field. Let's try something a little more complex.

An Age Converter
In addition to the BirthDate, I'd like to display the age of the Person as well. The problem is that we don't have an Age field. So, instead, we'll use the BirthDate field to calculate the age. We'll just add another class (AgeConverter) to the Converters.cs file.

(Click image to enlarge)

We won't worry about the specifics of the code too much (I'm sure that I'll get letters with better ways to calculate age). The main point is that we are taking a DateTime (BirthDate) and converting it to an Integer (Age). Well, actually, we're converting it to a string for display purposes, but you get the idea.

Using the AgeConverter is just the same as the DateConverter. Add a Static Resource (I called mine "myAgeConverter") and then use it in a Binding statement. Notice that we are still Binding to the BirthDate field; by using the AgeConverter, we control what we will actually display. Here is a portion of our expanded DataTemplate (we appended the Age portions to the section with the BirthDate).

(Click image to enlarge)

If you run the app now, you'll see both the BirthDate and the Age are included.

One More Converter
As one final step, we'll get a little more creative with our converter. So far, we've more or less just converted a value from one format to another. This time, we'll convert to a completely different type. I want to display ages that are over 30 in a green font and 30 or under in blue. We'll do this by changing the BirthDate to a Brush.

Once again, create a new class "AgeBrushConverter" in the Converter.cs file. The code looks like the "AgeConverter" code, but instead of returning the age, it uses it to create a SolidColorBrush:

(Click image to enlarge)

For the implementation, our final "Resources" section looks like this:

(Click image to enlarge)

And our final DataTemplate (notice that we are using the AgeBrushConverter for the Foreground property of the TextBlock):

(Click image to enlarge)

And our final result:

(Click image to enlarge)

We've taken a chance to look at DataTemplates and ValueConverters to add flexibility to how we display our data in ListBoxes. DataTemplates can contain complex layouts (including Grids and Images, if desired). ValueConverters also go beyond simple formatting (like Dates and Currency); they can also be used to show or hide items (think of Binding a value to the "Visible" property) or even to add text formatting to differentiate values (such as using red for negative numbers and black for positive, or possibly graying-out inactive items).

Hang on to this application. In Part 2, we'll take a look at creating a custom user control that abstracts out part of the functionality and exposes an event that we can use.