Silverlight Google Search API Source Code Sample

Referencing the JavaScript based Google Search API via Silverlight 2.0 isn't too difficult. The sample below was stripped out of our new Silverlight user interface for EggHeadCafe and refactored for easier public consumption.

I choose to the use the Google Search API in JavaScript versus Google REST service offering because the search results are different.  The JavaScript API appears to match google.com much more closely than the REST service does.  The key to implementing the Google Search API is understanding how to expose pieces of your Silverlight application to JavaScript and vice versa.  This is complicated by Google delivering its results via asynchronous calls to their search engine.  Read through the comments in the JavaScript code below to get a feel for how Silverlight and JavaScript can react to each other.   

You will need to change the googleapi.js script to reference your own google search API account when you implement this in your own application.  Here are a few blocks of important sample code that query the google api for the search phrase.  All of the code including the ability to page through search results is in the   source code download.

/googleapi/googlesilverlightapi.js

var googleSearchPageElement;
var googleSearch;
var SilverlightSearchControl = null;

// This function is called in the Silverlight server control tag of Silverlight.aspx OnPluginLoaded="pluginLoaded" event in order
// to set an easy to use reference to the Silverlight control via JavaScript.  Plus, we'll hook up
// a DOM element for the google api to reference.

function pluginLoaded(sender)
{
    SilverlightSearchControl = sender.get_element();
    googleSearchPageElement = new EggHeadCafeSearchLinks('googlesearchdiv');

}
 
function EggHeadCafeSearchLinks(search){ this.buildSearchControl(search); }
 
EggHeadCafeSearchLinks.prototype.buildSearchControl = function(search)
{
  googleSearch = new GwebSearch();
  googleSearch.name=search;
  googleSearch.setResultSetSize(GSearch.LARGE_RESULTSET);
  googleSearch.setNoHtmlGeneration();
  googleSearch.setSearchCompleteCallback(this, EggHeadCafeSearchLinks.prototype.searchComplete, [null]);
}
 
EggHeadCafeSearchLinks.prototype.execute = function(query){ googleSearch.execute(query);}
EggHeadCafeSearchLinks.prototype.clearAllResults = function () { googleSearch.clearAllResults; }

// Silverlight will use the HtmlPage.Invoke method to call this function and move the google API
// search result cursor to the desired page.  When google is finished, the searchComplete handler
// is triggered just like a normal search is completion event.
function GotoGoogleSearchResultPage(pageNumber) { googleSearch.gotoPage(pageNumber); }

// Silverlight will use the HtmlPage.Invoke method to call this function and initiate a new search.
function LoadGoogleSearchResults(searchTerm)
{
    googleSearchPageElement.clearAllResults();
    googleSearchPageElement.execute(searchTerm);

}

// Wire up the asynchronous event handler for the google search API.  It is fired
// when the search results have been returned or the results current page has changed.

EggHeadCafeSearchLinks.prototype.searchComplete = function()
{
   
resultarr = googleSearch.results;  // get an array of search results

    var cursor = googleSearch.cursor;  // get a reference to the pages collection.

    if (resultarr.length == 0)
    {
        // Using the Silverlight reference to our plug in, we'll reference the SearchViewModelResponse object.
        // This is a reference to our JavaScriptResponseHandler class in Silverlight (EggHeadCafe.Silverlight.SearchViewModel)
        // and call it's Send method.  The JavaScriptResponseHandler is registered in Silverlight by calling
        // HtmlPage.RegisterScriptableObject("SearchViewModelResponse",JavaScriptResponseHandler).

        // The first parameter is a type, which in this sample is always 1 for a google search, the second is your
        // own success or failure code, and the third is a text response used to send in XML to Silverlight.
      
  SilverlightSearchControl.Content.SearchViewModelResponse.Send(1, "failure", "");
        return;
    }

    // Use this ugly string concatenation code to create some XML based on search results for the currently
    // selected page of google results.

  
    var xml = '<?xml version="1.0"?><message><pageCount>' + cursor.pages.length + '</pageCount><records>';
    var xml2 = '</records></message>';

    for (i = 0; i < resultarr.length; i++)
    {
        xml += '<record><url><![CDATA[' + resultarr[i].unescapedUrl + ']]></url>';
        xml += '<description><![CDATA[' + resultarr[i].title + ']]></description>';
        xml += '<summary><![CDATA[' + resultarr[i].content + ']]></summary></record>';
    }

    // Send the results back to our Silverlight JavaScriptResponseHandler
   
SilverlightSearchControl.Content.SearchViewModelResponse.Send(1, "200", xml + xml2);

}




EggHeadCafe.Silverlight Project

using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Diagnostics;
using EggHeadCafe.DataObjects;
using EggHeadCafe.Xaml;

namespace EggHeadCafe.Silverlight
{
    public partial class Search : UserControl
    {

         public SearchViewModel ViewModel = new SearchViewModel();

        public Search()
        {
             InitializeComponent();
            this.DataContext = ViewModel;
             this.SubmitButton.Click += new RoutedEventHandler(ViewModel.OnSubmitButtonClick);
            ViewModel.PagerButtonSetting.PageCountChanged+= new EventHandler(ViewModel_SearchResultPageCountChanged);
         }

         void ViewModel_SearchResultPageCountChanged(object sender, EventArgs e)
        {
            this.PagerButtons.Children.Clear();

             if (ViewModel.PagerButtonSetting.PageCount < 1) return;

             Pager.LoadPagerButtons(this.PagerButtons,
                                   new RoutedEventHandler(ViewModel.OnSearchResultPagingButtonClick),
                                   ViewModel.PagerButtonSetting);

         }

        private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            var listBox = sender as ListBox;
             if (listBox == null) return;
            var item = listBox.SelectedItem as SearchResultItem;
             if (item == null) return;
            MessageBox.Show(item.Url);
        }

    }
}



using System;
using System.Windows.Browser;
using EggHeadCafe.WebBrowser;

namespace EggHeadCafe.Silverlight
{

    public class JavaScriptResponseHandler : EggHeadCafe.WebBrowser.JavaScriptResponseHandler   
    {
         public enum RequestTypes : int
        {
            Unknown = 0,
            SearchResult = 1
        }

         [ScriptableMember()]
        public void Send(RequestTypes type,string code,string response)
        {
             base.Send((int)type, code, response);
        }

    }
}

using System;
using System.Xml;
using System.Xml.Linq;
using System.Linq;
using System.Net;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.Windows.Browser;
using EggHeadCafe.Xaml;
using EggHeadCafe.WebBrowser;
using EggHeadCafe.DataObjects;

namespace EggHeadCafe.Silverlight
{
    public class SearchViewModel : INotifyPropertyChanged
    {

        private JavaScriptResponseHandler _javaScriptResponseHandler = new JavaScriptResponseHandler();
      
        public SearchViewModel()
        {
            SearchPhrase = "asp.net session";
            _javaScriptResponseHandler.ResponseReceived +=
                                   new EventHandler<JavaScriptEventArgs<JavaScriptResponse>>(OnJavaScriptResponseReceived);



HtmlPage.RegisterScriptableObject("SearchViewModelResponse", _javaScriptResponseHandler);
        }

        private ObservableCollection<SearchResultItem> _searchResultItems = new ObservableCollection<SearchResultItem>();
         public ObservableCollection<SearchResultItem> SearchResultItems
        {
            get { return _searchResultItems; }

             private set
            {
                _searchResultItems = value;
                 OnPropertyChanged("SearchResultItems");
            }
        }

         private PagerButtonSetting _pagerButtonSetting = new PagerButtonSetting();
        public PagerButtonSetting PagerButtonSetting
        {
            get { return _pagerButtonSetting; }

             private set
            {
                _pagerButtonSetting = value;
                 OnPropertyChanged("PagerButtonSetting");
            }
        }


        // Set the default to false to watch the binding on the button's IsEnabled property.
        private bool _submitButtonEnabled = true;
        public bool SubmitButtonEnabled
        {
            get { return _submitButtonEnabled; }
            set
            {
                _submitButtonEnabled = value;
                 OnPropertyChanged("SubmitButtonEnabled");
            }
        }

        private string _searchPhrase = string.Empty;
        public string SearchPhrase
        {
            get { return _searchPhrase; }
            set
            {
                _searchPhrase = value;
                 OnPropertyChanged("SearchPhrase");
            }
        }

        public void OnSubmitButtonClick(object sender, RoutedEventArgs e)
        {
            LoadSearchResults();
        }

        public void OnSearchResultPagingButtonClick(object sender, RoutedEventArgs e)
        {
            var button = sender as Button;
             if (button == null) return;
            
            var pagerButton = button.Tag as PagerButton;
             if (pagerButton == null) return;

            LoadSearchResults(pagerButton.CurrentPage);
        }

         private void LoadSearchResults()
        {
            LoadSearchResults(0);
        }

        private void LoadSearchResults(int searchResultsPage)
        {
            
            SearchResultItems.Clear();

            if (searchResultsPage == 0)
            {
                PagerButtonSetting.SelectedIndex = 1;
                PagerButtonSetting.PageCount = 0;
                 HtmlPage.Window.Invoke("LoadGoogleSearchResults", new string[] { "'" + SearchPhrase.Replace("'", "") + "'" });
                 return;
            }

            PagerButtonSetting.SelectedIndex = searchResultsPage;
             // Google Paging API expects a zero based index of pages but our pager button implementation isn't.
             HtmlPage.Window.Invoke("GotoGoogleSearchResultPage", new string[] { (searchResultsPage -1).ToString() });
        }

        private void OnJavaScriptResponseReceived(object sender, JavaScriptEventArgs<JavaScriptResponse> e)
         {
             if (e == null)  return;

             if (e.Response.Code != "200")
            {
                PagerButtonSetting.PageCount = 0;
                PagerButtonSetting.SelectedIndex = 0;
                 SearchResultItems.Add(new SearchResultItem(null,"No results found.",string.Empty));
                 return;
            }
// Use LINQ to parse the xml returned from the google API call.



var xml = XDocument.Parse(e.Response.Text);

            var records = from results in xml.Descendants("record")
            select results;
             foreach(var item in records)
            {                
                 SearchResultItems.Add(new SearchResultItem((string)item.Element("url").Value,
                                                           (string)item.Element("description").Value,
                                                           (string)item.Element("summary").Value));
            }

            PagerButtonSetting.PageCount = Convert.ToInt32(xml.Document.Element("message").Element("pageCount").Value)
                                                                                   * PagerButtonSetting.MaxRowsPerPage;
        }

        public event PropertyChangedEventHandler PropertyChanged;

         protected void OnPropertyChanged(string propertyName)
        {
             if (PropertyChanged == null) { return; }
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

    }
}



namespace EggHeadCafe.DataObjects
{

    public class SearchResultItem  
    {

         public SearchResultItem() { }

        public SearchResultItem(string url, string description, string summary)
        {
            Description = description;
            Summary = summary;
            Url = url;
        }

        public string Url { get; set; }

        private string _description = string.Empty;
        public string Description
        {
             get
             {
                  return _description;
             }
             set
             {
                 if (value != _description)
                {
                    _description = StripHtml(value);
                }
            }
        }

        private string _summary = string.Empty;
        public string Summary
        {
             get
             {
                  return _summary;
             }
             set
             {
                 if (value != _summary)
                {
                    _summary = StripHtml(value);
                 }
             }
        }

        private string StripHtml(string text)
        {
             if (String.IsNullOrEmpty(text)) return text;
            return text.Replace("<b>", "").Replace("</b>", "").Replace("'", "'");
        }
    
    }
}







EggHeadCafe.WebBrowser Project

using System;
using System.Windows.Browser;

namespace EggHeadCafe.WebBrowser
{

    public abstract class JavaScriptResponseHandler
    {

         public event EventHandler<JavaScriptEventArgs<JavaScriptResponse>> ResponseReceived;
      
         [ScriptableMember()]
         protected void Send(int type,string code,string response)
        {
             // This method is called in JavaScript not from inside Silverlight.  Think of it as exposing a public property
            // on the Silverlight plug-in control in the web page.  That is what ScriptableMember means...
            if (ResponseReceived == null) { return; }

             ResponseReceived(this, new JavaScriptEventArgs<JavaScriptResponse>(new JavaScriptResponse(type,code,response)));
        }

    }
}

using System;

namespace EggHeadCafe.WebBrowser
{
    public class JavaScriptResponse
    {
         public int Type { get; set; }
        public string Code { get; set; }
        public string Text { get; set; }

        public JavaScriptResponse(int type, string code, string text)
        {
            Type = type;
            Code = code;
            Text = text;
        }
     }
}

using System;

namespace EggHeadCafe.WebBrowser
{
    public class JavaScriptEventArgs<T> : System.EventArgs where T : JavaScriptResponse
    {

         public T Response { get; set; }
        
        public JavaScriptEventArgs(T response)  
        {
            Response = response;
        }

    }
}
By Robbe Morris   Popularity  (3886 Views)
Picture
Biography - Robbe Morris
Robbe has been a Microsoft MVP in C# since 2004. He is also the co-founder of NullSkull.com which provides .NET articles, book reviews, software reviews, and software download and purchase advice.  Robbe also loves to scuba dive and go deep sea fishing in the Florida Keys or off the coast of Daytona Beach. Microsoft MVP