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;
}
}
}