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:
- Visual Studio 2010 Beta 2
- Silverlight 4 Beta (which includes RIA Services)
You can download the completed solution as well. and be sure to check out the full talk.
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.
- public IQueryable<Restaurant> SearchRestaurants(string term)
- {
- //Find all plates that match, grouped by restaurant
- var restaurantPlatesList = this.ObjectContext.Restaurants
- .Where (r => r.Plates.Any (p=>p.Name.Contains(term)))
- .OrderBy(k => k.ID);
- foreach (var restaurant in restaurantPlatesList)
- {
- var plateList = ObjectContext.Plates
- .Where(p => p.RestaurantID == restaurant.ID)
- .Where(p => p.Name.Contains(term));
- foreach (var plate in plateList)
- restaurant.Plates.Add(plate);
- }
- //Find all restaurants that match
- var restaurantsList = this.ObjectContext.Restaurants
- .Where(r => r.Name.Contains(term) &&
- !restaurantPlatesList.Contains(r));
- return restaurantPlatesList.Concat(restaurantsList);
- }
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.
- internal sealed class RestaurantMetadata
- {
- [Include]
- public EntityCollection<Plate> Plates;
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:
- private void button1_Click(object sender, RoutedEventArgs e)
- {
- ContentFrame.Navigate(new Uri("/Search?term=" + searchBox.Text,
- UriKind.Relative));
- }
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:
After we get the UI laid out correctly you end up with a pager and all the columns set right.
Then wire up the parameter to the SearchRestaurants query method to the query string…
- protected override void OnNavigatedTo(NavigationEventArgs e)
- {
- this.restaurantDomainDataSource.QueryParameters.Add(
- new System.Windows.Data.Parameter()
- {
- ParameterName = "term",
- Value = NavigationContext.QueryString["term"]
- });
- }
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.
- <data:DataGrid.RowDetailsTemplate>
- <DataTemplate>
- <ListBox ItemsSource="{Binding Plates}">
- <ListBox.ItemTemplate>
- <DataTemplate>
- <StackPanel Orientation="Horizontal">
- <Image Source="{Binding Path=IconPath, Converter={StaticResource ImagePathConverter1}}"
- Margin="40,0,10,0"></Image>
- <StackPanel>
- <TextBlock Text="{Binding Path=Name}" Width="400"
- TextWrapping="Wrap"
- FontWeight="Bold"></TextBlock>
- <TextBlock Text="{Binding Path=Description}" Width="400"
- TextWrapping="Wrap"></TextBlock>
- <HyperlinkButton Click="HyperlinkButton_Click"
- Content="Details..."
- NavigateUri="{Binding ID}" />
- </StackPanel>
- </StackPanel>
- </DataTemplate>
- </ListBox.ItemTemplate>
- </ListBox>
- </DataTemplate>
- </data:DataGrid.RowDetailsTemplate>
Run it and we get this sort of view…
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.
- private void HyperlinkButton_Click(object sender, RoutedEventArgs e)
- {
- var button = sender as HyperlinkButton;
- var plate = button.DataContext as Plate;
- NavigationService.Navigate(
- new Uri("/Plates?restaurantId=" + plate.RestaurantID + "&" +
- "plateId=" + plate.ID,
- UriKind.Relative));
- }
Now we need to make a slight tweak to the Plates page because it does no know about the plateId query string parameter.
- protected override void OnNavigatedTo(NavigationEventArgs e)
- {
- //Handle RestaurantID
- plateDomainDataSource.QueryParameters.Add(
- new System.Windows.Data.Parameter()
- {
- ParameterName = "resId",
- Value = NavigationContext.QueryString["restaurantId"]
- });
- //Handle PlateID
- var qs = NavigationContext.QueryString;
- if (qs.ContainsKey("plateId"))
- {
- this.plateDomainDataSource.FilterDescriptors =
- new FilterDescriptorCollection();
- this.plateDomainDataSource.FilterDescriptors.Add(
- new FilterDescriptor("ID",
- FilterOperator.IsEqualTo, qs["plateId"]));
- }
- }
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:
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
Anonymous
November 25, 2009
Is there a way to integrate that with Full Text searching?Anonymous
November 28, 2009
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.Anonymous
December 12, 2009
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?Anonymous
December 14, 2009
The comment has been removedAnonymous
December 14, 2009
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.Anonymous
January 04, 2010
Thanks Brad for sharing.