Tuesday, May 12, 2009

Silverlight 2, WCF, and Lambda Expressions - Part 2

In Part 1, we set up a Silverlight 2 application that consumes a WCF Service. In Part 2, we'll look at a problem with the implementation and different ways of solving it -- ultimately ending with Lambda Expressions. Hopefully by the time we get done, you'll be comfortable with how lambda expressions work and how to use them to make your code cleaner and easy to understand.

Where We Left Off
Here's a quick reminder of what we did last time:
  1. We created a WCF Service that is Silverlight 2 compatible.
  2. We added a reference to the service in a Silverlight application.
  3. We created a UI (Button and ListBox) to call the service and show the results.
  4. We called the WCF Service and hooked up the Completed event to an event handler.

The Page.xaml.cs file we finished with is as follows:

(Click image to enlarge)

And the UI:

(Click image to enlarge)

A Problem
There is a small problem with the UI. After calling the service (by clicking the button), the list gets populated with the names. You can select a name in the list by clicking on it. But if you click the button again, the list refreshes and the selection goes away. We want to update this so that the selected item is maintained when the service is called again.

Solution #1 - Using the Event Handler
The SelectedItem of the ListBox gets cleared when the service is called because we are re-binding the data. From the ListBox's point of view, it is getting a new object to bind to. When the old object is un-bound, the selected item goes away and doesn't come back.

We'll solve this by simply saving off the SelectedIndex of the ListBox before we make the service call. Then after the service call returns, we will reassign the SelectedIndex. (This is assuming that the order of our items is not changing, and we're not getting any new items -- you can extend this basic functionality for more complex situations).

Since we need to save off the SelectedIndex in one method (the Button Click event handler) and then use it in a different method (the Service Completed event handler), we'll need to create a class-level variable to hold this information. See the code below:

(Click image to enlarge)

You'll see 3 new lines of code to get this to work:
  1. private int ehListIndex;
    The class-level variable
  2. ehListIndex = EHList.SelectedIndex;
    Saving the index
  3. EHList.SelectedIndex = ehListIndex;
    Re-assigning the index to the list box

If you re-run the application, you will see that if you make a selection in the list box, that selection is maintained when you call the service multiple times.

Solution #2 - Using an Anonymous Delegate
Now, we'll try this again using an anonymous delegate instead of an explicit event handler. You'll see why this is an advantage in just a bit.

We'll update our XAML by adding another StackPanel to our Grid.

(Click image to enlarge)

You'll notice that this section looks almost identical to the EventHandler UI section. Here's a summary of the differences:

  • The 2nd column of the Grid is specified in the StackPanel.
  • The Names of the ListBox and Button start with AD (for anonymous delegate) instead of EH.
  • The ListBox border is blue (just to make it different).
  • The button Content is "Anonymous Delegate".
  • There is a new Click handler for the button (you can look at the hint in Part 1 about letting IntelliSense create the handler for you).

So, what is an anonymous delegate? In our case, it is an in-line definition of an event handler (for the service callback). This is defined by using the "delegate" keyword, then a set of parentheses with the appropriate parameters, then a set of curly braces with the method implementation. The implementation will look just like in our callback when using the EventHandler.

Here's the initial implementation:

(Click image to enlarge)

What you will notice is that that the first and last lines are the same as our previous button click event. You'll also notice that the parameters for the anonymous delegate ("object" and "GetPeopleCompletedEventArgs") matches the parameters in the explicit event handler we implemented earlier. Finally, the body of the delegate matches the body of the event handler callback.

I know what you're thinking: big deal. Other than "in-lining" some code, exactly what did we gain here? We're about to find out.

We still have the same issue we had earlier with the ListBox SelectedIndex. The advantage to using an anonymous delegate is that any variables in the outer method can be accessed in the delegate implementation. This means that instead of using a class-level variable to store the index, we can use a local variable. Check out the completed code below:

(Click image to enlarge)

You'll see that we now have a local variable named "adListIndex" that is assigned outside of the delegate and then used inside of the delegate. We've just eliminated the need to have a class-level variable.

Solution #3 - Using a Lambda Expression
Let's take this one step further. Instead of using an anonymous delegate, we'll use a lambda expression. First, update the UI. This XAML is very similar to the anonymous delegate section:

(Click image to enlarge)

The same items are updated as above: grid column, element names, border color, button content, and click event.

So, now lets take a look at lambda expressions. There are 2 types of lambda expressions: statement lambdas and expression lambdas. Both use the same syntax (consisting of 3 parts):
  1. Parameters
    These are normally enclosed by parentheses (although, parentheses can be excluded if there is only a single parameter).
    The parameter types may be excluded if the compiler can determine the correct types.
  2. =>
    This is known as the "goes to" operator and denotes that this is a lambda.
  3. Statement(s) or Expression(s)
    A statement lambda has one or more operations wrapped in curly braces. The statement(s) denotes some type of work to be done.
    An expression lambda has one or more expressions (that return true or false) wrapped in curly braces. These are used extensively in LINQ.
    If there is only a single statement or expression, then the curly braces can be excluded.
We'll be working with an Statement Lambda. The first steps to convert the anonymous delegate to a lambda expression are to remove the "delegate" keyword and place the "=>" operator between the parameters and the method implementation. Take a look at the "LEButton_Click" method below and compare it to the first "ADButton_Click" method above:

(Click image to enlarge)

But, as we noted above, if the compiler can determine the parameter types, then we don't need to include them in our code:

(Click image to enlarge)

If you hover over the "s" and "ea" parameters, you will see that Visual Studio recognizes these as being of type "object" and "GetPeopleCompletedEventArgs" respectively. This means that we still have strongly-typed objects. The same is true of "ea.Result". This is the strongly-typed collection that is returned from our WCF Service.

As a final step, we can add the SelectedIndex handling code into the mix:

(Click image to enlarge)

If you run the application, you will see that all three list boxes behave the same way. All three list boxes will keep the selected index between service calls.
What We Learned
We took a look at 3 different ways to solve a particular UI issue. We see that both the Anonymous Delegate and Lambda Expression solutions have the advantage of being able to use a local-scoped variable (thus, keeping our class-level clutter-free). In addition, we see that the Lambda Expression solution has the advantage of not needing to include the parameter types. This is especially helpful if you have a delegate that has the standard 2 parameter signature ("object" and "SomeKindOfEventArgs"); you don't need to know the particular EventArgs class -- just let Visual Studio figure that out for you.
My advice: become familar with Lambda Expressions. You will find that when used judiciously, they can help keep your code understandable and easier to maintain.

No comments:

Post a Comment