Silverlight and RIA Services: Implementing Search

Some of you likely noticed that my PDC09 demo included a stubbed out Search function that I didn’t really get to walkthrough during the talk.   I thought I’d do a blog post showing how it is done.

To get started you need:

You can download the completed solution as well.  and be sure to check out the full talk.

 

image

 

First let’s write a new method on the DomainService to return the search results.  In this case I want to return any Plates who’s name matches (grouped by Restaurant)  and any Restaurant who’s name matches. 

  1. public IQueryable<Restaurant> SearchRestaurants(string term)
  2. {
  3.     //Find all plates that match, grouped by restaurant
  4.     var restaurantPlatesList = this.ObjectContext.Restaurants
  5.         .Where (r => r.Plates.Any (p=>p.Name.Contains(term)))
  6.         .OrderBy(k => k.ID);
  7.     foreach (var restaurant in restaurantPlatesList)
  8.     {
  9.         var plateList = ObjectContext.Plates
  10.             .Where(p => p.RestaurantID == restaurant.ID)
  11.             .Where(p => p.Name.Contains(term));
  12.         foreach (var plate in plateList)
  13.             restaurant.Plates.Add(plate);
  14.     }
  15.  
  16.     //Find all restaurants that match
  17.     var restaurantsList = this.ObjectContext.Restaurants
  18.         .Where(r => r.Name.Contains(term) &&
  19.                     !restaurantPlatesList.Contains(r));
  20.  
  21.     return restaurantPlatesList.Concat(restaurantsList);
  22.  
  23. }         
  24.  

In line 1, you see that we are defining a new query that returns Restaurants… this one has a different name, and takes a search term as an argument. 

In line 4-6, we are getting all the restaurants that that have any plates that match the search term.

In line 7-14, we are looping through all those restaurants and manually adding the plates that match to the collection we will return.  Effectively, we are manually creating the instance in just the shape we need it for the client.

In line 17-18, we are getting the Restaurants that match and that are not already included. 

and finally, in line 21, we return the concatenation of the two queries. 

Notice that this query is designed to return Restaurants AND the Plates that are related.   By default, related entities are not included (in order to save bandwidth).. so we need to go into the metadata file and explicitly include them.

  1. internal sealed class RestaurantMetadata
  2. {
  3.     [Include]
  4.     public EntityCollection<Plate> Plates;
  5.  
  6.  

 

Now, in the Silverlight client.  First we need to wire up the Search button on MainPage.xaml..   There we just need to follow the same pattern we saw in the earlier post:

  1. private void button1_Click(object sender, RoutedEventArgs e)
  2. {
  3.     ContentFrame.Navigate(new Uri("/Search?term=" + searchBox.Text,
  4.                             UriKind.Relative));
  5. }
  6.  

Then the search page, again we build it the same way as we saw in the earlier post, by simply drag and dropping from the data sources window.   Notice that we now have two different query methods for Restaurants.  So we simply select the right one and then drag and drop on the form as we saw in the earlier post:

image

After we get the UI laid out correctly you end up with a pager and all the columns set right.

image

Then wire up the parameter to the SearchRestaurants query method to the query string…

  1. protected override void OnNavigatedTo(NavigationEventArgs e)
  2. {
  3.     this.restaurantDomainDataSource.QueryParameters.Add(
  4.     new System.Windows.Data.Parameter()
  5.     {
  6.         ParameterName = "term",
  7.         Value = NavigationContext.QueryString["term"]
  8.     });
  9. }

Now i’d like to display the Plates for each Restaurant that is returned.  To do that, i’ll make use of the RowDetails feature of DataGrid.

  1. <data:DataGrid.RowDetailsTemplate>
  2.         <DataTemplate>
  3.             <ListBox ItemsSource="{Binding Plates}">
  4.                 <ListBox.ItemTemplate>
  5.                     <DataTemplate>
  6.                         <StackPanel Orientation="Horizontal">
  7.                             <Image Source="{Binding Path=IconPath, Converter={StaticResource ImagePathConverter1}}"
  8.                                     Margin="40,0,10,0"></Image>
  9.                             <StackPanel>
  10.                                 <TextBlock Text="{Binding Path=Name}" Width="400"
  11.                                             TextWrapping="Wrap"
  12.                                             FontWeight="Bold"></TextBlock>
  13.                                 <TextBlock Text="{Binding Path=Description}" Width="400"
  14.                                            TextWrapping="Wrap"></TextBlock>
  15.                                 <HyperlinkButton Click="HyperlinkButton_Click"
  16.                                                  Content="Details..."
  17.                                                  NavigateUri="{Binding ID}" />
  18.                             </StackPanel>
  19.                         </StackPanel>
  20.                     </DataTemplate>
  21.                 </ListBox.ItemTemplate>
  22.             </ListBox>
  23.  
  24.         </DataTemplate>
  25.     </data:DataGrid.RowDetailsTemplate>

Run it and we get this sort of view…

image

Notice the URL includes the search term, so I can send this around in email to share my search results bookmark it for future reference. 

Now, you might want to drill into the details on what of these plates… so let’s handle that “Details..” hyperlink.  In the code behind for the search page, we handle navigating to the Plates page with a the right query string paramaters.

  1. private void HyperlinkButton_Click(object sender, RoutedEventArgs e)
  2. {
  3.     var button = sender as HyperlinkButton;
  4.     var plate = button.DataContext as Plate;
  5.  
  6.     NavigationService.Navigate(
  7.         new Uri("/Plates?restaurantId=" + plate.RestaurantID + "&" +
  8.                 "plateId=" + plate.ID,
  9.         UriKind.Relative));
  10. }
  11.  

 

Now we need to make a slight tweak to the Plates page because it does no know about the plateId query string parameter. 

  1. protected override void OnNavigatedTo(NavigationEventArgs e)
  2. {
  3.     //Handle RestaurantID
  4.     plateDomainDataSource.QueryParameters.Add(
  5.         new System.Windows.Data.Parameter()
  6.         {
  7.             ParameterName = "resId",
  8.             Value = NavigationContext.QueryString["restaurantId"]
  9.         });
  10.  
  11.  
  12.     //Handle PlateID
  13.     var qs = NavigationContext.QueryString;
  14.     if (qs.ContainsKey("plateId"))
  15.     {
  16.         this.plateDomainDataSource.FilterDescriptors =
  17.             new FilterDescriptorCollection();
  18.         this.plateDomainDataSource.FilterDescriptors.Add(
  19.               new FilterDescriptor("ID",
  20.                   FilterOperator.IsEqualTo, qs["plateId"]));
  21.     }
  22. }
  23.  

 

The first few lines to handle the RestaurantID were already there, so we just needed to add the code to handle the PlateID..  Notice we don’t need to change the query method on the server for this, we just add a new where clause that will get sent to the server and executed there. 

The result:

image

Again, notice the URL, something we can bookmark or send around in email, etc. 

 

I hope you got something valuable from this walkthrough… You can download the completed solution as well and be sure to check out the full talk.

 

Have fun!