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!

No comments:

Post a Comment