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!

Comments (6)

  1. Paulio says:

    Is there a way to integrate that with Full Text searching?

  2. MarkC says:

    Hi Brad,

    I’ve been trying to find an example of how to used the CompositionAttribute with CUD operations when using LinqToEntitiesDomainService. I can’t find one anywhere and really need this functionality in the next couple of days. Do you know of any examples or could you put together a quick example for me? The only resonable example I can find is at:

    http://blogs.msdn.com/digital_ruminations/archive/2009/11/18/composition-support-in-ria-services.aspx

    but it is based on L2S.

    Thanks in advance,

    Mark.

  3. NelsonD says:

    I’ve been working with this code, but find that the NavigationContext.QueryString["restaurantId"] is case sensitive.  This makes it’s usefulness in a RESTful environment alot less useful.  I know that IDictionary allows for case-insensitive comparisions.  Any reason why this wasn’t done in this case?

  4. BradA says:

    NelsonD — thanks for your comments… here is word back from the folks that own the Navigation framework:

    IDictionary doesn’t actually allow for case-insensitive comparisons.  That’s an argument to the Dictionary constructor, and he should be able to easily solve this himself with this line of code:

    var ignoreCaseQueryString = new Dictionary<string, string>(NavigationContext.QueryString, StringComparer.CurrentCultureIgnoreCase);

    Fundamentally, the choice was made not to do this by default because we don’t want to make any assumptions about the requirements you want to place on your code.  We give you the raw data, and it’s fully up to you what you wish to do with it 

  5. NelsonD says:

    Thanks for the response Brad.  I understand the reasoning, I just feel it’s incorrect in the Navigation Framework.  If someone wants to see the menu for "ReStauRantID=1", I sincerely doubt your code, no matter how robust, is going to find it.  In any case, we work with what is, not with what we want.

    Thanks again.

  6. lam seo says:

    Thanks Brad for sharing.