Thursday, June 11, 2009

Hovering Bing Search using jQuery

Taking the Bing API for another test drive, I wrote up a jQuery / JSON Ajax driven website tool as well. The following little thing will show a semi-transparent hovering box as you select text on a web site. If you click the box, a Bing search will be made, and the top 10 results will be shown in the popup.




Feel free to give it a go at the example page. The initial JavaScript is shown in full shortly, but for updated source code and examples, I suggest you head over to the HoverBing CodePlex project I just created.


(function()
{
getSelectedText = function()
{
if(window.getSelection){
return window.getSelection().toString();
}
else if(document.getSelection){
return document.getSelection();
}
else if(document.selection){
return document.selection.createRange().text;
}
}

alternate = function(counter, norm, alt)
{
return counter % 2 == 0 ? alt : norm;
}

createHoverBingObject = function(settings)
{
HoverBingObject =
{
// Settings
appId: settings.appId,
numResults: settings.numResults,
sources: settings.sources,
title: settings.title,

activateSearchPopup: function(x, y)
{
var popup = $("#SearchPopup");
if(x && y)
{
popup
.css({
cursor: "pointer",
left: x + 5,
top: y + 5
})
.show();
HoverBingObject.repositionToFitScreen();
HoverBingObject.fadePopupByDistance(x, y);
}
popup.data("SearchPopup_State", "waiting");
$().bind('mousemove', HoverBingObject.mouseMoveSearchPopup);
},

create: function()
{
HoverBingObject.initSearchPopup();
$().mouseup(function(e) {
var selText = getSelectedText();
var popup = $("#SearchPopup");
var state = popup.data("SearchPopup_State");
if((state == undefined || state == "hidden") &&
selText.length > 0)
{
popup.data("SearchPopup_Query", HoverBingObject.washQueryText(selText));
HoverBingObject.activateSearchPopup(e.pageX, e.pageY);
}
else if(state != "stapled")
{
HoverBingObject.deactivateSearchPopup();
}
});
$().mousedown(function(e) {
var popup = $("#SearchPopup");
var state = popup.data("SearchPopup_State");
if(state != undefined && state == "waiting")
{
HoverBingObject.deactivateSearchPopup();
}
});
},

deactivateSearchPopup: function()
{
var popup = $("#SearchPopup");
popup.find("span").empty();
popup
.fadeTo(0, 0)
.hide()
.data("SearchPopup_State", "hidden");
$().unbind('mousemove', HoverBingObject.mouseMoveSearchPopup);
},

fadePopupByDistance: function(mouseX, mouseY)
{
var popup = $("#SearchPopup");
var pos = popup.position();
pos.left += popup.width() * 0.5;
pos.top += popup.height() * 0.5;
var dist = Math.round(Math.sqrt(Math.pow(mouseX - pos.left, 2) + Math.pow(mouseY - pos.top, 2)));
popup.fadeTo(0, Math.max(1 - dist / 500, 0));
},

initSearchPopup: function()
{
// Create box
var box = document.createElement("div");
$(box)
.attr("id", "SearchPopup")
.attr("class", "HoverBing")
.css({
position: "absolute",
opacity: 0
})
.mouseenter(function() {
var state = $("#SearchPopup").data("SearchPopup_State");
if(state == "waiting")
{
HoverBingObject.stapleSearchPopup();
}
})
.mouseleave(function() {
var popup = $("#SearchPopup");
var state = popup.data("SearchPopup_State");
if(state == "stapled")
{
HoverBingObject.activateSearchPopup();
}
})
.data("SearchPopup_State", "hidden");

// Create header control
var header = document.createElement("div");
$(header)
.attr("class", "Header")
.html(HoverBingObject.title);

// Create content control
var content = document.createElement("span");
$(content)
.attr("class", "Content")

// Append header and content to outer box
$(box)
.append(header)
.append(content);

// Add outer box to body
$(document.body).append(box);
},

mouseMoveSearchPopup: function(e)
{
HoverBingObject.fadePopupByDistance(e.pageX, e.pageY);
},

onClickSearchPopup: function()
{
var popup = $("#SearchPopup");
var query = popup.data("SearchPopup_Query");
var contents = popup.find("span").empty();
$.getJSON("http://api.search.live.net/json.aspx" +
"?AppId=" + HoverBingObject.appId +
"&Market=en-US&Query=" + query +
"&Sources=" + HoverBingObject.sources +
"&Web.Count=" + HoverBingObject.numResults +
"&JsonType=callback&JsonCallback=?",
HoverBingObject.onSearchResultsReceived);
},

onSearchResultsReceived: function(data)
{
var popup = $("#SearchPopup");
var contents = popup.find("span").empty();

if(data.SearchResponse == null)
{
contents.html("No search results returned");
return;
}

$.each(data.SearchResponse.Web.Results, function(i ,item) {
contents.append(
$(document.createElement("a"))
.attr("class", alternate(i, "ResultLine", "ResultLineAlt"))
.css({
display: 'block'
})
.click(function() {
HoverBingObject.deactivateSearchPopup();
})
.mouseenter(function() {
$(this).addClass("ResultLineHover");
})
.mouseleave(function() {
$(this).removeClass("ResultLineHover");
})
.text(item.Title)
.attr("href", item.Url)
.attr("target", "_blank")
);
});

HoverBingObject.repositionToFitScreen();
},

repositionToFitScreen: function()
{
var popup = $("#SearchPopup");
var pos = popup.position();
if(pos.left + popup.width() > $(window).width())
{
popup.css({left: $(window).width() - popup.width()});
}
if(pos.top + popup.height() > $(window).height())
{
popup.css({top: $(window).height() - popup.height()});
}
},

stapleSearchPopup: function()
{
$("#SearchPopup")
.data("SearchPopup_State", "stapled")
.fadeTo(0, 1)
.one('click', HoverBingObject.onClickSearchPopup);
$().unbind('mousemove', HoverBingObject.mouseMoveSearchPopup);
},

washQueryText: function(text)
{
return escape(text.replace(/[^'"A-z0-9]/g, "+")).substr(0, 100);
}
}
}

window.HoverBing = function(options)
{
settings = jQuery.extend({
title: "Click to Bing Search",
appId: "96AE4D816B34AA03F44EEBC53F4C23F9A146C011",
numResults: 10,
sources: "web"
}, options);

if(typeof(HoverBingObject) == 'undefined')
{
createHoverBingObject(settings);
HoverBingObject.create();
}
}
})();


And there you have it. To use it, link the jQuery-1.3.2 javascript, include the above JavaScript, and the CSS styles found on the CodePlex site linked from the top, then activate it by using a jQuery ready handler such as:

$(function() { HoverBing(); });

Writing a Silverlight powered Bing news web part for SharePoint

Microsoft recently unveiled their Bing search engine, and swiftly followed up by exposing an API free of request count limits. Grabbing that opportunity to write a really simple news fetcher web part for SharePoint, I set out to attach a Silverlight client to the Bing API, using Windows Communication Foundation (WCF).

Getting started with Bing

Essentially all you need to use Bing in your .NET apps, is an API id, which is available from the Bing developer site. Specifically, you can create an app id here.

The developer site also hosts a bunch of other resources, such as how to get started with other clients (using JSON or XML and so forth), so if you're interested - check that out.

Setting up your client

I chose to write a client in Silverlight, for a bunch of reasons, really. First of all I like how easy it is to wrap Silverlight apps together in Visual Studio, and second, it's really simple to make them shine using Blend. So while I could use jQuery, or just about any other technology, Silverlight pulled me in solely on how easy it is to get something working.

If you wish to test the Bing api from a console app, after creating your app id, you can do so by:

  1. Create a new console project in Visual Studio.
  2. Add a service reference to the Bing web services (http://api.search.live.net/search.wsdl?AppID=YourAppIdHere).
  3. Name the service namespace BingService, for simplicity.
  4. Slap the following code in there (be sure to replace the app id there as well):


namespace TestApp
{
using System;
using BingService;

internal class Program
{
private static void Main(string[] args)
{
var bingClient = new LiveSearchPortTypeClient();
SearchResponse response = bingClient.Search(new SearchRequest
{
AppId = "YOUR APP ID HERE",
Query = "einaros",
Sources = new[] {SourceType.Web}
});
foreach (WebResult result in response.Web.Results)
{
Console.Out.WriteLine(result.Title + ": " + result.Url);
}
Console.WriteLine("Done");
Console.ReadKey();
}
}
}


Running that should give you a bunch of url's you'd never wish to browse to.

Going all Silverlighty

Now that you've got the basics going, it's time to pull out your Silverlight. If you haven't installed the developer sdk / runtime and such, now would be a great time to do so. Head over to the Silverlight developer site for instructions on how.

With the environment setup, create a new Silverlight application in Visual Studio. Be sure to answer "yes" when you're asked whether to create a new test project as well. You'll need this to access the web services.

If you named your project anything like BingSilverNews, you should now see two projects; BingSilverNews and BingSilverNews.Web. The latter you won't have to touch at all, so feel free to collapse that.

The main project, however, should get the same service reference as previously added.

With the service reference added, head to your Page.xaml markup, and add a simple templated listbox as follows:

<UserControl x:Class="BingSilverNews.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid x:Name="LayoutRoot" Background="White">
<ListBox x:Name="NewsResults">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Vertical">
<TextBlock Text="{Binding Path=Title}" FontWeight="Bold"/>
<TextBlock Text="{Binding Path=Snippet}" TextWrapping="Wrap" Width="300"/>
<HyperlinkButton NavigateUri="{Binding Path=Url}"><TextBlock Text="Read More"/></HyperlinkButton>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>



If you're entirely unfamiliar with Silverlight, you may want to give Scott Guthrie's tutorial a go. There's really not much complexity to the above construct though.

What we're doing is adding a ListBox, and calling that "NewsResults", to ensure that we've got somewhere to put our results.

The Silverlight listbox however knows precious little about Bing, news results or anything remotely like that. It knows how to stack items in a neat little list, and that's it. To simplify handling of results from Bing, we're going to instruct it on how to handle each line of the results, using an item template.

Notice the DataTemplate above. The contents of that block will be repeated for each item in the Bing search result array. It's got a stack panel, which basically puts UI elements on top of, or alongside, each other, and two text blocks and a hyperlink. The key part of making the template work, is obviously tying the output to something from the active Bing search result line. That's where the Bindings come in. If you look to the title textblock, the "{Binding Path=Title}" simply makes the template fetch the value for the textblock's text attribute from the Title field of whatever object is being processed. Repeat that explanation for the rest of the template's controls, and we can move on. I'll soon get back to where the listbox (and the templates) get the data from.

Now turning to the codebehind, we're going to create a search service instance once more, and do a timed search every 30 seconds or so.

namespace BingSilverNews
{
using System;
using System.Threading;
using System.Windows.Controls;
using BingService;

public partial class Page : UserControl
{
private readonly LiveSearchPortTypeClient _bingClient;
private readonly Timer _updateTimer;

public Page()
{
InitializeComponent();

_bingClient = new LiveSearchPortTypeClient();
_bingClient.SearchCompleted += OnSearchCompleted;
_updateTimer = new Timer((state) => OnTimer());
_updateTimer.Change(new TimeSpan(0), new TimeSpan(0, 0, 0, 30));
}

private void OnSearchCompleted(object sender, SearchCompletedEventArgs e)
{
if ((e.Result != null) &&
(e.Result.News != null) &&
(e.Result.News.Results != null))
{
// We've got news!
NewsResults.ItemsSource = e.Result.News.Results;
}
}

private void OnTimer()
{
if (CheckAccess())
{
_bingClient.SearchAsync(new SearchRequest
{
Sources = new[] {SourceType.News},
Query = "Microsoft",
AppId = "YOUR APP ID"
});
}
else
{
Dispatcher.BeginInvoke(OnTimer);
}
}
}
}


The source should resemble that of the first search client we made way up. The only difference here, is that we do it on a timer, and assign the result to a Silverlight ListBox, rather than just writing each result to console.

Worth noting is the fact that Silverlight requires an asynchronous service call model. That means that each call you make to a service, will return immediately. Only upon call to a predefined callback will the remote call actually have happened (and any return value be brought back). In this case, that's happens through the _bingClient.SearchCompleted property, which is told to execute OnSearchCompleted every time a search completes. The actual search call is now called _bingClient.SearchAsync(), rather than Search. Other than that, the search client behaves pretty much the same as in the previous app.

The timer does have some voodoo to it. Since it's happening on a separate thread from the UI, and we're updating UI controls as a result of the service call, we have to jump about a bit. This resembles the InvokeRequired semantics from WinForms: Only the UI thread can update it's controls - anything else will result in an exception, to avoid concurrent (and crashing) updates to the same controls. Because of this, we have to check for access to the UI on each OnTimer call, and if not granted, have the UI dispatcher schedule to call OnTimer once more in its own context. In other words, the UI and timer will run in different threads. Once the timer is signaled on it's thread, it will tell the UI to execute the _bingClient.SearchAsync call in its own thread.

Upon completion of each search, the search result array (noted by e.Result.News.Results), which has items of the NewsResult class, each holding Title, Snippet and Url fields, is assigned to the listbox. What will happen is that the listbox processes the array, one item at a time, and instantiates the contents of the xaml's DataTemplate for each one. Remembering that the template adressed the Title, Snippet and Url fields in its bindings, it's now obvious where these come from.

Compiling and running the app should result in something like the following(only my results are localized for Norwegian Bing):


Using this in SharePoint

Getting Silverlight going in SharePoint is a slightly discomforting process. That's why someone took the time to write the Silverlight in SharePoint Blueprints. While all the steps there aren't strictly required, it does get the job done.

Once you've made the necessary changes to your portal, I also suggest you adobt the brilliant WSPBuilder to speed up the process of writing (and deploying) webparts. There's a walkthrough of the WSPBuilder Visual Studio add-in here, which should get you going even quicker.

Once you've got this all setup, wrapping up the Silverlight web part is quite simple. Create a new WSPBuilder project, and add a new WSPBuilder Web Part with Feature item to that project. Name the project (and webpart) something meaningful, for your own good.

In my example setup, I've expanded the structure of the WSPBuilder project as follows:


Note the LAYOUTS and BingNews folders added under the TEMPLATE folder. Putting the .xap-file from your Silverlight project here (or better yet, adding a pre-build step to copy it to that project folder, from your Silverlight project's output folder), will make the WSPBuilder solution deploy it to the relative layouts folder of portal you deploy to. The .xap *is* the Silverlight, so this step is required for anything to work properly beyond here.

You'll also want to change the .NET runtime version for the project to 3.5, and reference the System.Web.Silverlight assembly, as well as System.Web.Extensions.

Heading back to the codebehind of your new webpart project, it needs a few (but not many) changes:

namespace Grep.SharePoint.BingNews
{
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.SilverlightControls;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;

[Guid("your guid")]
public class BingSearch : WebPart
{
private bool _error;
private ScriptManager _scriptManager;

public BingSearch()
{
ExportMode = WebPartExportMode.All;
}

/// <summary>
/// Create all your controls here for rendering.
/// Try to avoid using the RenderWebPart() method.
/// </summary>
protected override void CreateChildControls()
{
if (!_error)
{
try
{
base.CreateChildControls();
var sl = new Silverlight();
sl.ID = ID + "_SL";
sl.Width = Unit.Percentage(100);
sl.Height = Height;
sl.Source = "~/_layouts/BingNews/Grep.Silverlight.BingNews.xap";
Controls.Add(sl);
}
catch (Exception ex)
{
HandleException(ex);
}
}
}

/// <summary>
/// Clear all child controls and add an error message for display.
/// </summary>
/// <param name="ex"></param>
private void HandleException(Exception ex)
{
_error = true;
Controls.Clear();
Controls.Add(new LiteralControl(ex.Message));
}

/// <summary>
/// Ensures that a script manager exists on the page.
/// </summary>
/// <param name="e"></param>
protected override void OnInit(EventArgs e)
{
if (!_error)
{
try
{
base.OnInit(e);
_scriptManager = ScriptManager.GetCurrent(Page);
if (_scriptManager == null)
{
_scriptManager = new ScriptManager();
_scriptManager.ID = "ScriptManager1";
Controls.AddAt(0, _scriptManager);
if (Page.Form != null)
{
Page.Form.Controls.AddAt(0, _scriptManager);
}
else
{
throw new Exception("No form tag found on current page. ScriptManager cannot be added.");
}
}
}
catch (Exception ex)
{
HandleException(ex);
}
}
}

/// <summary>
/// Ensures that the CreateChildControls() is called before events.
/// Use CreateChildControls() to create your controls.
/// </summary>
/// <param name="e"></param>
protected override void OnLoad(EventArgs e)
{
if (!_error)
{
try
{
base.OnLoad(e);
EnsureChildControls();
}
catch (Exception ex)
{
HandleException(ex);
}
}
}
}
}


This really looks a lot like the default WSPBuilder template, with a couple of modifications. I've removed the web part properties (you could keep them, though, and use those to pass keywords to your Silverlight Bing client, using the Silverlight control's InitParams), and I've added an OnInit override which adds a ScriptManager to the page - if that's not there already. The Silverlight control requires a ScriptManager to spawn Silverlight instances on the fly, so this is also a quite required step.

I've also changed the webpart's base class to System.Web.UI.WebControls.WebParts.WebPart (which is recommended by Microsoft, for all new web parts).

Other than that, all it does is create a Silverlight control instance, setup the source, and initialize the Silverlight's widt / height from the webpart's own width / height, and add it to the control stack.

Building the source, wrapping a WSP (using the WSPBuilder project context menu), and deploying it to your SharePoint portal. Then activating the feature, and adding the webpart, should add a simple little Bing News box to your portal. Expanding it further, I leave up to you. Being Silverlight, you can add all kinds of fancy effects to the UI, if you please, and even test it from an outside-of-SharePoint web project.

Monday, June 1, 2009

New and Improved Template Library Connector

If you're new to the Template Library Connector, here's the nutshell version:
Grep's Template Library Connector adds the option to use a SharePoint document library as a template library for other document libraries. The templates are hierarchically shown in the "New" menu submenus of the document libraries, and instantiates exactly like common document templates. Changes to the template library are instantly available through the "New" menu of *all* linked document libraries.

There's an older, a bit more technical post available here, with install instructions.

Downloads (source and binary) is available at the CodePlex project site: Grep's Template Library Connector.

News for v1.5 include:
  • Full WSS support
  • No more webpart to connect document libraries: all config done from the list's settings page.
  • Will work correctly with webpart pages hosting multiple document library views (ListViewWebParts)
From v1.0:
  • Connect any document library to another document library, with one serving template documents to the other.
  • Not require any administration of new templates, other than uploading them to the template library - Instantly making them available to all document libraries linked with the template library.
  • Allow document libraries and template libraries to reside in different sites.
  • Supports Content Types in the template and document libraries, with workflow connections, columns, data information panels and the other goodies that provides.
  • Make the templates instantly, and hierarchically, available from the "New" menu in the document library.


Demonstration Video