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.

  1: (function()
  2: {
  3:     getSelectedText = function()
  4:     {
  5:         if(window.getSelection){
  6:             return window.getSelection().toString();
  7:         }
  8:         else if(document.getSelection){
  9:             return document.getSelection();
 10:         }
 11:         else if(document.selection){
 12:             return document.selection.createRange().text;
 13:         }
 14:     }   
 15: 
 16:     alternate = function(counter, norm, alt)
 17:     {
 18:         return counter % 2 == 0 ? alt : norm;
 19:     }
 20: 
 21:     createHoverBingObject = function(settings) 
 22:     {
 23:         HoverBingObject = 
 24:         {
 25:             // Settings
 26:             appId: settings.appId,
 27:             numResults: settings.numResults,
 28:             sources: settings.sources,
 29:             title: settings.title,
 30:             
 31:             activateSearchPopup: function(x, y)
 32:             {
 33:                 var popup = $("#SearchPopup");
 34:                 if(x && y)
 35:                 {
 36:                     popup
 37:                         .css({
 38:                             cursor: "pointer",
 39:                             left: x + 5,
 40:                             top: y + 5
 41:                         })
 42:                         .show();
 43:                     HoverBingObject.repositionToFitScreen();
 44:                     HoverBingObject.fadePopupByDistance(x, y);
 45:                 }
 46:                 popup.data("SearchPopup_State""waiting");
 47:                 $().bind('mousemove', HoverBingObject.mouseMoveSearchPopup);
 48:             },
 49: 
 50:             create: function() 
 51:             {
 52:                 HoverBingObject.initSearchPopup();
 53:                 $().mouseup(function(e) {
 54:                         var selText = getSelectedText();
 55:                         var popup = $("#SearchPopup");
 56:                         var state = popup.data("SearchPopup_State");
 57:                         if((state == undefined || state == "hidden") && 
 58:                            selText.length > 0)
 59:                         {
 60:                             popup.data("SearchPopup_Query", HoverBingObject.washQueryText(selText));
 61:                             HoverBingObject.activateSearchPopup(e.pageX, e.pageY);
 62:                         }
 63:                         else if(state != "stapled")
 64:                         {
 65:                             HoverBingObject.deactivateSearchPopup();
 66:                         }
 67:                     });
 68:                 $().mousedown(function(e) {
 69:                         var popup = $("#SearchPopup");
 70:                         var state = popup.data("SearchPopup_State");
 71:                         if(state != undefined && state == "waiting")
 72:                         {
 73:                             HoverBingObject.deactivateSearchPopup();
 74:                         }
 75:                     });
 76:             },
 77: 
 78:             deactivateSearchPopup: function()
 79:             {
 80:                 var popup = $("#SearchPopup");
 81:                 popup.find("span").empty();
 82:                 popup
 83:                     .fadeTo(0, 0)
 84:                     .hide()
 85:                     .data("SearchPopup_State""hidden");
 86:                 $().unbind('mousemove', HoverBingObject.mouseMoveSearchPopup);
 87:             },
 88: 
 89:             fadePopupByDistance: function(mouseX, mouseY)
 90:             {
 91:                 var popup = $("#SearchPopup");
 92:                 var pos = popup.position();
 93:                 pos.left += popup.width() * 0.5;
 94:                 pos.top += popup.height() * 0.5;
 95:                 var dist = Math.round(Math.sqrt(Math.pow(mouseX - pos.left, 2) + Math.pow(mouseY - pos.top, 2)));
 96:                 popup.fadeTo(0, Math.max(1 - dist / 500, 0));
 97:             },
 98:             
 99:             initSearchPopup: function()
100:             {
101:                 // Create box
102:                 var box = document.createElement("div");
103:                 $(box)
104:                     .attr("id""SearchPopup")
105:                     .attr("class""HoverBing")
106:                     .css({
107:                             position: "absolute",
108:                             opacity: 0
109:                         })
110:                     .mouseenter(function() {
111:                             var state = $("#SearchPopup").data("SearchPopup_State");
112:                             if(state == "waiting"
113:                             {
114:                                 HoverBingObject.stapleSearchPopup();
115:                             }
116:                         })
117:                     .mouseleave(function() {
118:                             var popup = $("#SearchPopup");
119:                             var state = popup.data("SearchPopup_State");
120:                             if(state == "stapled"
121:                             {
122:                                 HoverBingObject.activateSearchPopup();
123:                             }
124:                         })
125:                     .data("SearchPopup_State""hidden");
126:                 
127:                 // Create header control
128:                 var header = document.createElement("div");
129:                 $(header)
130:                     .attr("class""Header")
131:                     .html(HoverBingObject.title);
132: 
133:                 // Create content control
134:                 var content = document.createElement("span");
135:                 $(content)
136:                     .attr("class""Content")
137:                 
138:                 // Append header and content to outer box
139:                 $(box)
140:                     .append(header)
141:                     .append(content);
142:                     
143:                 // Add outer box to body
144:                 $(document.body).append(box);
145:             },
146: 
147:             mouseMoveSearchPopup: function(e)
148:             {
149:                 HoverBingObject.fadePopupByDistance(e.pageX, e.pageY);
150:             },
151: 
152:             onClickSearchPopup: function()
153:             {
154:                 var popup = $("#SearchPopup");
155:                 var query = popup.data("SearchPopup_Query");
156:                 var contents = popup.find("span").empty();
157:                 $.getJSON("http://api.search.live.net/json.aspx" + 
158:                             "?AppId=" + HoverBingObject.appId + 
159:                             "&Market=en-US&Query=" + query + 
160:                             "&Sources=" + HoverBingObject.sources +
161:                             "&Web.Count=" + HoverBingObject.numResults + 
162:                             "&JsonType=callback&JsonCallback=?"
163:                             HoverBingObject.onSearchResultsReceived);
164:             },
165: 
166:             onSearchResultsReceived: function(data)
167:             {
168:                 var popup = $("#SearchPopup");
169:                 var contents = popup.find("span").empty();
170: 
171:                 if(data.SearchResponse == null
172:                 {
173:                     contents.html("No search results returned");
174:                     return;
175:                 }
176:                 
177:                 $.each(data.SearchResponse.Web.Results, function(i ,item) {
178:                         contents.append(
179:                                 $(document.createElement("a"))
180:                                     .attr("class", alternate(i, "ResultLine""ResultLineAlt"))
181:                                     .css({
182:                                             display: 'block'
183:                                         })
184:                                     .click(function() {
185:                                         HoverBingObject.deactivateSearchPopup();
186:                                     })
187:                                     .mouseenter(function() {
188:                                             $(this).addClass("ResultLineHover");
189:                                         })
190:                                     .mouseleave(function() {
191:                                             $(this).removeClass("ResultLineHover");
192:                                         })
193:                                     .text(item.Title)
194:                                     .attr("href"item.Url)
195:                                     .attr("target""_blank")
196:                             );
197:                     });
198:                     
199:                 HoverBingObject.repositionToFitScreen();
200:             },
201:             
202:             repositionToFitScreen: function()
203:             {
204:                 var popup = $("#SearchPopup");
205:                 var pos = popup.position();
206:                 if(pos.left + popup.width() > $(window).width())
207:                 {
208:                     popup.css({left: $(window).width() - popup.width()});
209:                 }
210:                 if(pos.top + popup.height() > $(window).height())
211:                 {
212:                     popup.css({top: $(window).height() - popup.height()});
213:                 }
214:             },
215: 
216:             stapleSearchPopup: function()
217:             {
218:                 $("#SearchPopup")
219:                     .data("SearchPopup_State""stapled")
220:                     .fadeTo(0, 1)
221:                     .one('click', HoverBingObject.onClickSearchPopup);
222:                 $().unbind('mousemove', HoverBingObject.mouseMoveSearchPopup);
223:             },
224:             
225:             washQueryText: function(text)
226:             {
227:                 return escape(text.replace(/[^'"A-z0-9]/g"+")).substr(0, 100);
228:             }
229:         }
230:     }
231:     
232:     window.HoverBing = function(options)
233:     {
234:         settings = jQuery.extend({
235:                 title: "Click to Bing Search",
236:                 appId: "96AE4D816B34AA03F44EEBC53F4C23F9A146C011",
237:                 numResults: 10,
238:                 sources: "web"
239:             }, options);
240: 
241:         if(typeof(HoverBingObject) == 'undefined')
242:         {
243:             createHoverBingObject(settings);
244:             HoverBingObject.create();
245:         }
246:     }
247: })();


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:

  1: $(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):
  1: namespace TestApp
  2: {
  3:     using System;
  4:     using BingService;
  5: 
  6:     internal class Program
  7:     {
  8:         private static void Main(string[] args)
  9:         {
 10:             var bingClient = new LiveSearchPortTypeClient();
 11:             SearchResponse response = bingClient.Search(new SearchRequest
 12:                                                             {
 13:                                                                 AppId = "YOUR APP ID HERE",
 14:                                                                 Query = "einaros",
 15:                                                                 Sources = new[] {SourceType.Web}
 16:                                                             });
 17:             foreach (WebResult result in response.Web.Results)
 18:             {
 19:                 Console.Out.WriteLine(result.Title + "" + result.Url);
 20:             }
 21:             Console.WriteLine("Done");
 22:             Console.ReadKey();
 23:         }
 24:     }
 25: }


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:
  1: <UserControl x:Class="BingSilverNews.Page"
  2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  4:     <Grid x:Name="LayoutRoot" Background="White">
  5:         <ListBox x:Name="NewsResults">
  6:             <ListBox.ItemTemplate>
  7:                 <DataTemplate>
  8:                     <StackPanel Orientation="Vertical">
  9:                         <TextBlock Text="{Binding Path=Title}" FontWeight="Bold"/>
 10:                         <TextBlock Text="{Binding Path=Snippet}" TextWrapping="Wrap" Width="300"/>
 11:                         <HyperlinkButton NavigateUri="{Binding Path=Url}"><TextBlock Text="Read More"/></HyperlinkButton>
 12:                     </StackPanel>
 13:                 </DataTemplate>
 14:             </ListBox.ItemTemplate>
 15:         </ListBox>
 16:     </Grid>
 17: </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.
  1: namespace BingSilverNews
  2: {
  3:     using System;
  4:     using System.Threading;
  5:     using System.Windows.Controls;
  6:     using BingService;
  7: 
  8:     public partial class Page : UserControl
  9:     {
 10:         private readonly LiveSearchPortTypeClient _bingClient;
 11:         private readonly Timer _updateTimer;
 12: 
 13:         public Page()
 14:         {
 15:             InitializeComponent();
 16: 
 17:             _bingClient = new LiveSearchPortTypeClient();
 18:             _bingClient.SearchCompleted += OnSearchCompleted;
 19:             _updateTimer = new Timer((state) => OnTimer());
 20:             _updateTimer.Change(new TimeSpan(0), new TimeSpan(0, 0, 0, 30));
 21:         }
 22: 
 23:         private void OnSearchCompleted(object sender, SearchCompletedEventArgs e)
 24:         {
 25:             if ((e.Result != null) &&
 26:                 (e.Result.News != null) &&
 27:                 (e.Result.News.Results != null))
 28:             {
 29:                 // We've got news!
 30:                 NewsResults.ItemsSource = e.Result.News.Results;
 31:             }
 32:         }
 33: 
 34:         private void OnTimer()
 35:         {
 36:             if (CheckAccess())
 37:             {
 38:                 _bingClient.SearchAsync(new SearchRequest
 39:                                             {
 40:                                                 Sources = new[] {SourceType.News},
 41:                                                 Query = "Microsoft",
 42:                                                 AppId = "YOUR APP ID"
 43:                                             });
 44:             }
 45:             else
 46:             {
 47:                 Dispatcher.BeginInvoke(OnTimer);
 48:             }
 49:         }
 50:     }
 51: }


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:
  1: namespace Grep.SharePoint.BingNews
  2: {
  3:     using System;
  4:     using System.Collections.Generic;
  5:     using System.ComponentModel;
  6:     using System.Runtime.InteropServices;
  7:     using System.Web.UI;
  8:     using System.Web.UI.SilverlightControls;
  9:     using System.Web.UI.WebControls;
 10:     using System.Web.UI.WebControls.WebParts;
 11: 
 12:     [Guid("your guid")]
 13:     public class BingSearch : WebPart
 14:     {
 15:         private bool _error;
 16:         private ScriptManager _scriptManager;
 17: 
 18:         public BingSearch()
 19:         {
 20:             ExportMode = WebPartExportMode.All;
 21:         }
 22: 
 23:         /// <summary>
 24:         /// Create all your controls here for rendering.
 25:         /// Try to avoid using the RenderWebPart() method.
 26:         /// </summary>
 27:         protected override void CreateChildControls()
 28:         {
 29:             if (!_error)
 30:             {
 31:                 try
 32:                 {
 33:                     base.CreateChildControls();
 34:                     var sl = new Silverlight();
 35:                     sl.ID = ID + "_SL";
 36:                     sl.Width = Unit.Percentage(100);
 37:                     sl.Height = Height;
 38:                     sl.Source = "~/_layouts/BingNews/Grep.Silverlight.BingNews.xap";
 39:                     Controls.Add(sl);
 40:                 }
 41:                 catch (Exception ex)
 42:                 {
 43:                     HandleException(ex);
 44:                 }
 45:             }
 46:         }
 47: 
 48:         /// <summary>
 49:         /// Clear all child controls and add an error message for display.
 50:         /// </summary>
 51:         /// <param name="ex"></param>
 52:         private void HandleException(Exception ex)
 53:         {
 54:             _error = true;
 55:             Controls.Clear();
 56:             Controls.Add(new LiteralControl(ex.Message));
 57:         }
 58: 
 59:         /// <summary>
 60:         /// Ensures that a script manager exists on the page.
 61:         /// </summary>
 62:         /// <param name="e"></param>
 63:         protected override void OnInit(EventArgs e)
 64:         {
 65:             if (!_error)
 66:             {
 67:                 try
 68:                 {
 69:                     base.OnInit(e);
 70:                     _scriptManager = ScriptManager.GetCurrent(Page);
 71:                     if (_scriptManager == null)
 72:                     {
 73:                         _scriptManager = new ScriptManager();
 74:                         _scriptManager.ID = "ScriptManager1";
 75:                         Controls.AddAt(0, _scriptManager);
 76:                         if (Page.Form != null)
 77:                         {
 78:                             Page.Form.Controls.AddAt(0, _scriptManager);
 79:                         }
 80:                         else
 81:                         {
 82:                             throw new Exception("No form tag found on current page. ScriptManager cannot be added.");
 83:                         }
 84:                     }
 85:                 }
 86:                 catch (Exception ex)
 87:                 {
 88:                     HandleException(ex);
 89:                 }
 90:             }
 91:         }
 92: 
 93:         /// <summary>
 94:         /// Ensures that the CreateChildControls() is called before events.
 95:         /// Use CreateChildControls() to create your controls.
 96:         /// </summary>
 97:         /// <param name="e"></param>
 98:         protected override void OnLoad(EventArgs e)
 99:         {
100:             if (!_error)
101:             {
102:                 try
103:                 {
104:                     base.OnLoad(e);
105:                     EnsureChildControls();
106:                 }
107:                 catch (Exception ex)
108:                 {
109:                     HandleException(ex);
110:                 }
111:             }
112:         }
113:     }
114: }


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

Saturday, May 30, 2009

Using Document Libraries as Template Libraries


Download the source / wsp at CodePlex

Update June 1st: Version 1.5 released. The new version no longer requires a webpart, nor a lot of configuration, so I've updated the outlines seen in this post. See this new post for more info on the update.

Background

SharePoint document libraries are capable of handling many day-to-day tasks, right out of the box; there's no question about that. When you approach the need or wish for simple to maintain template libraries, that's an entirely different story.

A usual approach is to configure content types, and a set of hard-to-maintain URL links between these and actual documents in other locations. Administrative tasks, such as adding and removing templates, quickly becomes next to impossible, as you'll have to create new content types, and add /remove these manually for each document library you want the templates available in.

Another issue is the fact that the "New" menu for document libraries will show all content types at the root level. You can't categorize them in submenus, so large amounts of templates will be everything but a smooth ride for the end user.

Fixing things with a simple feature

Realizing that the aforementioned widespread pain is completely unnecessary, I set out yesterday to write a feature which can:

  • 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.
  • Make the templates instantly, and hierarchically, available from the "New" menu in the document library.

.. And make it all available to all of you, complete with source code, pre-built solution/feature, instructions and documentation.

Installation and Usage

Download the latest source and install packages here.

  1. Unpack and run the install.bat file from the root folder. This will install the solution, and deploy it to all site collections. If you want more fine-grained control, feel free to install and deploy the .wsp-file on your own.
  2. Open a SharePoint portal with document libraries you want to template enable.
  3. Open the Site Settings, and navigate to the Site Feature page.

  4. Enable "Grep's SmartDocument Template Library Connector"

  5. Open a document library you want to expose templates in, and open it's settings.

  6. Navigate to the "Template Library Connector settings" entry, under "General Settings".

  7. Make sure you select "Yes" to enable template library connections, pick a document library to act as a template library, and save.

  8. Navigate back to the document library, and verify that the "New" menu is wired up properly.

  9. Adding new documents to the selected template library will instantly make new templates available from the "New" menu.
  10. You can optionally add a column called "Description" to the template document library, for additional text under each template, in the "New" menu view. If you wish to call this something other than "Description", the column name can be changed through the webpart settings shown above.

And that should be it. If you've got any questions or problems, feel free to drop me a note through the comments here.

Getting Technical

Writing a solution like this takes a few steps, and a bit of tinkering with undocumented SharePoint libraries. Here's a rough cut of what's covered in the source:

  • Utilizing delegate controls to install handlers for all opened pages
  • Find all ListViewWebParts on the webpart page, and retrieve entries from its toolbar.
  • Adding items to ListViewWebPart's menus
  • Adding submenus to the "New" menu
  • Adding submenus to submenus
  • Utilizing the default SharePoint document template JavaScripts to create new documents
  • Retrieving hierarchies from SharePoint document libraries
  • Writing and exposing list settings pages

A few of these steps, such as adding menus and submenus, are powered by extension libraries I've included in the source code. A simple thing like adding a submenu to a submenu isn't easily and readily available in the SharePoint object model, but with the extension libraries in the source project, it's ridiculously simple!

Code Pieces

I won't be covering everything here, but some key parts of the functionality, which I haven't seen elsewhere on the web, will be brought forward.

Extending the LiewViewWebPart's "New" menu

The ListViewWebPart has a toolbar, but this isn't immediately accessible from its object model. Why this is so is oblivious to me, as I see many possible benefits of extending the default views.

Ignoring the ListViewWebPart's attempt at hiding it, and the fact that even toolbar access functions are internal, I extended the System.Web.UI.Control class to give me the following edge:

  1: public static T FindChildByType<T>(this Control self)
  2:     where T : class
  3: {
  4:     var stack = new Stack(self.Controls);
  5:     while (stack.Count > 0)
  6:     {
  7:         var control = stack.Pop() as Control;
  8:         if (control is T)
  9:         {
 10:             return control as T;
 11:         }
 12:         foreach (Control child in control.Controls)
 13:         {
 14:             stack.Push(child);
 15:         }
 16:     }
 17:     return null;
 18: }


The approach is pretty straight forward: it will do a stack-based iteration of the control class, until a control of the target type is found.

Including this allows you to call a function such as FindChildByType() on any class deriving from System.Web.UI.Control. Doing just that on the ListViewWebPart will, with no further hassle, let you access its inner NewMenu.

Once you get an instance of the NewMenu class (which derives from the superbly named ToolBarMenuButton class), you can call functions such as AddMenuItem and AddMenuItemSeparator (see MSDN). There's however no function to add submenus (or submenus to submenus, for that matter), so that will require another extension.

  1: public static SubMenuTemplate AddSubMenuAt(this ToolBarMenuButton self, int index, string id, string displayName,
  2:                                            string imageUrl, string description)
  3: {
  4:     SubMenuTemplate child = CreateSubMenu(id, displayName, imageUrl, description);
  5:     self.MenuTemplateControl.Controls.AddAt(index, child);
  6:     return child;
  7: }


The key part in the above code is the call to CreateSubMenu, which returns a SubMenuTemplate. The latter is a contruct from the Microsoft.SharePoint.WebControls namespace, which is used for several menus around the SharePoint UI, such as the Site Settings menu you get in MOSS when the "Office SharePoint Server Publishing" feature is enabled.

  1: private static SubMenuTemplate CreateSubMenu(string id, string displayName,
  2:                                              string imageUrl, string description)
  3: {
  4:     var child = new SubMenuTemplate();
  5:     child.ID = id;
  6:     child.Text = displayName;
  7:     child.Description = description;
  8:     if (string.IsNullOrEmpty(imageUrl))
  9:     {
 10:         imageUrl = "/_layouts/images/MenuNewItem.gif";
 11:         child.ImageUrl = imageUrl;
 12:     }
 13:     else
 14:     {
 15:         if (imageUrl.IndexOf("/") == -1)
 16:         {
 17:             imageUrl = "/_layouts/images/" + imageUrl;
 18:         }
 19:         child.ImageUrl = imageUrl;
 20:     }
 21:     return child;
 22: }


With these functions, along with a few more included in the source project, such as extending SubMenuTemplate to allow addition of nested submenus; adding menus and submenus is easy as pie.

Adding meaningful menu items

Getting to add menu items is one thing, but actually adding something meaningful is what obviously makes it useful. In this webpart, the goal was to make it possible to instantiate document (templates) from other document libraries, and that turns out to be surprisingly simple if you study how Microsoft does regular template instantiation. The javascript function called "createNewDocumentWithProgID" is the key here, and it accepts such parameters as source file url and target folder url. If the menu items are passed calls to this javascript in its click handler, with the correct urls, document instantiation is in the bag as well.

In Closing

So that's about it. Feel free to check the code, use the webpart, develop it further and share alike. For any questions or comments, use the comment feature on this blog.

Monday, May 18, 2009

Effective SharePoint UI prototyping using jQuery, FireBug and Greasemonkey

Demo script download available at the end of the post.
Updated May 19th, with clearer code examples.

Background

Customizing SharePoint's user interface, or even updating your own interfaces, can be a tiresome thing to do. Usually you'll have to deal with multiple master pages, many master page overrides, multiple css files and a wide array of dynamic code from XSLTs or web parts. All this makes it quite difficult to maintain a clear understanding and overview of the UIs you're customizing, or what the impact of your changes will be.

To add to the already high stress levels, many changes - especially those where you're deploying changes to your own web parts - will require recompiles, iis resets, feature reactivation or similar time consuming steps.

In this article I want to propose an alternative approach to UI changes, using jQuery prototyping. While jQuery isn't necessarily something you'd want to use to do all SharePoint UI restructuring, it does come in handy if you want to make simple runtime enhancements to the current UI, or even want to extend your custom parts with simple asynchronous behavior, or neat visual effects.

To accomplish this, in a way which will require no recompilation, redelpoyment or other nasty things, and overall keep the time between change and prototype visualization as low as possible; we'll be using the FireFox addons FireBug and Greasemonkey. The former is a brilliant DOM / CSS / Html / Network explorer, built into FireFox' interface, with lightning quick structure inspection and navigation. Greasemonkey, on the other hand, can inject custom javascript into any loaded page, without making any serverside changes.

The combination of these tools will allow us to write advanced javascripts in our favorite javascript editor, and see the results immediately, without having to upload any files to the server. A simple browser refresh will suffice.

Requirements

  • FireFox (3.0.10 is the latest at the time of writing)
  • FireBug
  • Greasemonkey
  • Knowledge of JavaScript and HTML / DOM.
Installing
  1. Download and install FireFox, if you haven't done this already.
  2. Start FireFox, and open the Addons config screen, available from the Tool menu.
  3. Activate "Get Addons" function on the top bar, then search for and install FireBug and Greasemonkey.
  4. Restart FireFox.
  5. Navigate to a SharePoint portal you wish to prototype, and confirm that you've got a set of FireBug / Greasemonkey icons in your browser status bar, such as:
  6. Right click the Greasemonkey icon, make sure it's set to enabled, and click New User script:

  7. Fill in values for the namespace and script title, such as:

    The "Includes" box should be the url to the page you wish to prototype. This can be changed later, so stick with the default value for now. In other words, don't copy this from my example above.
  8. Next you'll be asked to select which editor to use for script editing. This can be any text editor of your choice, such as notepad, or in my case, Visual Studio.
  9. Once the editor selection is completed, it will be opened, you'll see an empty script. To test that you've got it all setup correctly, you can try adding an alert statement, and reload the page.
  10. Next click the FireBug icon, and activate the HTML function:
  11. If you're unfamiliar with FireBug, play around with the Inspect function, to see how various UI elements correspond to the HTML source code. Selecting an element also enables you to immediately see the active CSS styles for said element.

Prototyping something useful

Assignment: Making the Quick Launch on the left collapsible.

"Useful" is relative, I agree, but this is at least example use of how to hotplug jQuery into the mix, and how you can use that to experiment with UI changes you'd later wish to deploy to a master page, or portal page.

From here on we'll concentrate mainly on the script opened in your chosen editor. As of now, all the content should be a commented "UserScript" section - which is good. If you've entered anything else, feel free to remove that now.

So, without further stalling, lets code.

Loading scripts (such as jQuery) at runtime

Greasemonkey lets us inject custom scripts, but rather than messing any more around with it's configuration, we'll do the rest of the script loading with code. Since this is a prototype, and we'll be doing all of the prototyping in FireFox; loading scripts is simple.

The only complicating step, is the fact that Greasemonkey will execute Greasemonkey scripts in a sandbox, isolating certain functionality from the scripts already loaded in the browser. The reasoning here is that Greasemonkey, and its scripts, will execute with greater privileges than those loaded by the browser. To prevent browser-loaded scripts from getting unwanted access from your custom Greasemonkey scripts, and possible do naughty stuff on your system, Greasemonkey forces us to take certain actions to access the stuff outside the sandbox. If you wish to read more about that, search for "unsafeWindow" in your favorite search engine.

I've put together a set of functions, which will load custom scripts in order, and map the objects from these into the namespace available from the Greasemonkey sandbox. The functions looks as follows, and you can go ahead and input these in your prototype script. Reading and understanding them are optional steps -- we'll soon be getting to the interesting stuff.

  1: function mapUnsafeObjects(objectArray) {
  2:     if (objectArray == null) {
  3:         return;
  4:     }
  5: 
  6:     for (var i in objectArray) {
  7:         var objectName = objectArray[i];
  8:         window[objectName] = unsafeWindow[objectName];
  9:     }
 10: }
 11: 
 12: function loadScript(url, objectArray, loadCallback) {
 13:     var js = document.createElement('script');
 14:     js.src = url;
 15:     js.type = 'text/javascript';
 16:     js.wrappedJSObject.onload = function() {
 17:         mapUnsafeObjects(objectArray);
 18:         loadCallback();
 19:     };
 20:     document.getElementsByTagName('head')[0].appendChild(js);
 21: }
 22: 
 23: function loadScripts(scriptArray, completeCallback) {
 24:     if (scriptArray.length == 0) {
 25:         completeCallback();
 26:     }
 27:     var scriptEntry = scriptArray.shift();
 28:     loadScript(scriptEntry.url, scriptEntry.objects, function() { loadScripts(scriptArray, completeCallback); });
 29: }


So these are the only utility functions you need to load external scripts. You can reuse them in any prototyping scripts you feed into Greasemonkey, to load whatever external content you need.

Here's an example on how to load the jQuery, jQuery UI and JSON2. We won't be needing more than the first in the following prototype, but if you wish to play around with jQuery UI (which has some cool drag and drop features, among other things), or the ajax + JSON functionality in jQuery, the others will be handy.

  1: loadScripts([
  2:              {url: "http://jquery.com/src/jquery-latest.js", objects: ["jQuery""$"]},
  3:              {url: "http://jquery-ui.googlecode.com/svn/tags/latest/ui/minified/jquery-ui.min.js", objects: []},
  4:              {url: "http://www.json.org/json2.js", objects: ["JSON"]}
  5:             ], onLoadComplete);


What this will do, is load one script at a time, making sure it's completely loaded before skipping to the next in line. For each script loaded, an array of objects is mapped into the sandbox namespace, for easy access in the prototype script. Once all scripts are loaded, the callback function named at the end will be called.

First jQuery action

Thus, the onLoadComplete function will be where our action will take place. Once this is called, jQuery will be fully loaded, and available for use. To demonstrate this, create a callback such as follows:

  1: function onLoadComplete() 
  2: {
  3:     $("a").css({ fontSize: '18pt' });
  4: }


Once you save this, along with the previous code snippets, in the Greasemonkey script opened in your chosen browser, and reload the portal, all html anchor elements will get a ridiculous font size. Not too useful, but it at least confirms everything is working properly. If the jQuery syntax is unfamiliar to you, now would be a good time to read up on some basic examples at jQuery.com.

Collapsible Quick Launch

This is when FireBug will come in handy. Writing the code is simple, and Greasemonkey is great, but without a proper tool to navigate the html, we'd be banging heads into walls before long.

With the FireBug window opened, and pointed to "HTML" mode, hit Inspect and select the section just below a main quick launch link, as shown in this screen shot:



Doing this tells us that the quick launch main menu items have IDs such as "zz2_QuickLaunchMenunN", where N is the menu item index. Tapping into this with jQuery is pretty simple, with a selector such as $("tr[id^=zz2_QuickLaunchMenun]"). What we'd like to do with this, is to attach a click handler to collapse / expand the table row below it, if the following table row isn't another main menu item.

Adding such a handler, in a pretty straight-forward manner, would look like:

  1: function onLoadComplete() {
  2:     $(function() {
  3:         $("tr[id^=zz2_QuickLaunchMenun]")
  4:             .next("[id=]").toggle()
  5:             .prev().click(function(evt) {
  6:                 $(this).next().toggle();
  7:                 evt.preventDefault();
  8:             });
  9:     });
 10: }


This function will install a handler to be run once the DOM tree is loaded and ready for manipulation: the "$(function(){})" syntax. This handler will, noted by line number:
3
Select all tr nodes with an id beginning with "zz2_QuickLaunchMenun". Hence forth called "main items".
4
For each of these main items, select the next sibling, if it has an empty id attribute. The empty id is an important point, as we don't want one main item to collapse the following main item, in the case of there being no child menu between the two. If there's no child menu, lines 5-8 will do nothing more for this main item. In this case, the selector from line 3 will bring us to the next main item instead.
4
Toggle (collapse) the child menu.
5
Select the sibling's previous item, which brings the selection state back to the main item
5
Install a click handler to the main item.
6
Use the toggle() function from jQuery to collapse or expand the sibling (child menu).
7
Simply prevents the click handler from bubbling further on to other nodes down the tree.
If jQuery is still new to you, you may just have noticed that jQuery selectors are cascading. Selecting one node, then doing another selector expression on that, will make a relative selection. That's how we, in the previous example, can move from the parent to the child, then back to the parent.

Saving and reloading the page, should confirm that the above code is working. When you click the background of the main quick launch items, the section will collapse or expand.

To wrap things up, we're going to add another piece of UI code, to animate the font size of the quick launch menu items, as they are mouse hovered. Expand / replace the function to resemble the following:

  1: function onLoadComplete() {
  2:     $(function() {
  3:         $("tr[id^=zz2_QuickLaunchMenun]")
  4:             .mouseenter(function(evt) {
  5:                 $(this).find("a").animate({ fontSize: '14px' });
  6:             })
  7:             .mouseleave(function(evt) {
  8:                 $(this).find("a").animate({ fontSize: '11px' });
  9:             })
 10:             .next("[id=]").toggle()
 11:             .prev().click(function(evt) {
 12:                 $(this).next().toggle();
 13:                 evt.preventDefault();
 14:             });
 15:     });
 16: }


Saving the script and reloading the page will actually uncover a bug in our previous selector, which wrongfully assumed that only the main items of the quick launch menu have the previously mentioned IDs. As it happens, sub items also employ this ID. While the click handler won't be affected (due to the sibling check), the mouseenter and mouseleave hover effect will be. Hovering the menu will now animate everything, while we want only the main items to be affected.

Pulling FireBug back up real quick will show that the items we actually want to attach to, have a parent table with an ID of "zz2_QuickLaunchMenu". To fix the issue we can thus take advantage of a more specific jQuery selector syntax: $("#zz2_QuickLaunchMenu > tbody > tr[id^=zz2_QuickLaunchMenun]").

This tells jQuery to look for a tr node with an id which starts with "zz2_QuickLaunchMenun", that has a direct parent element of type tbody, which in turn has a direct parent element with id "zz2_QuickLaunchMenu". Saving and reloading the page will confirm that the hover effect now only affects the main menu items.

Where to go from here

What I've demonstrated is a way to install and setup a hotpluggable SharePoint UI development environment. jQuery, and various other scripts, have vast possibilities for expanding SharePoints UI, and essential to exploring these, is a way to doing so quickly, without redeploying a bunch of stuff to the development environment (possibly polluting that, with nonsensical experiments).

Once you're done with the prototyping, you would obviously have to deploy jQuery, or whatever scripts you use, to the portal. Either in shape of a master page change, a new aspx page, web parts page template or similar. How you should deploy it depends largely on what you're installing, and what you want it to override.

There's no doubt in my mind that SharePoint development can benefit from such hotplug prototyping, however. Both the previously blogged about CodeConsole web part, and this simple prototyping environment, can - if used with care - simplify your life as a SharePoint developer or designer. Any opinions on how it could be done differently, are much obliged of course!

Downloads



MyProtoType.js

The final example code, with a decent looking expander icon next to the collapsible rows.

CodeConsole Minor Update

The CodeConsole Web Part just got a minor update, to allow simple inclusion of other assemblies and namespaces. This came in handy when I was debugging some SPContext-dependent functionality in a few libraries I deployed to a portal's bin dir.

In the new version, the source / wsp of which is still available here, the syntax is updated to support '@' prefixing for namespaces, and '#' for assemblies. Such as:

@MyLibrary
#Some.NameSpace.From.MyLibrary
MyClass foo = new MyClass();
output.Write(foo.SomeFunction());


Where the first MyLibrary reference would be an assembly in e.g. the bin folder of the current web application, or the GAC. No .dll suffix required.

Wednesday, May 13, 2009

CodeConsole Web Part for SharePoint

About the CodeConsole Web Part
Download Source Code + WSP

This is a web part I've found myself wanting several times, when I've been either too lazy to compile and deploy code, or haven't had the development tools available on the target SharePoint box.

In short, the CodeConsole Web Part ..

  • Executes C# code, entered into a text area, on the server. The code can access the SharePoint object model, and other .NET / ASP.NET objects.
  • Displays output from the execution in an output text area.
  • Uses jQuery to make asynchronous requests to a custom .asmx service on the server.
  • Uses JSON to serialize / deserialize data, should more complex structures be required by someone who decides to use it.
A Screen Shot of the Web Part in action:

CodeConsole Screen Shot
Click the image for a larger version
Update: Uploaded a demonstration screen capture video, in case a screen shot isn't enough. Check it out.

In this shot, the input frame is white, and the output frame is black. The code passed through the input box will have the following using directives available to it by default:
  • using System;
  • using System.IO;
  • using System.Text;
  • using Microsoft.SharePoint;
This can easily be extended in the attached code, otherwise you'll have to use fully qualified names in the code.

For output redirection, the code entered will have an object called 'output' available. This is a System.IO.TextWriter, meaning it will have such functions as Write and WriteLine. The above example screen shot uses this output object, so turn to that if you're confused.

Installing the web part is pretty straight forward, but will (due to the jQuery / JSON use) involve a few manual config steps. See my previous blog post for more information on that. Within the source code package, there's a folder called "wsp", within which you'll find a precompiled solution file, which can be deployed to a working SharePoint 3.0 portal.

About the implementation

The code isn't complex at all, and I'm sure many would point out that I could have done things a little bit differently here and there (such as rewriting how the web part is displayed, and taking optional configuration input to decide whether or not to link the json / jquery javascripts - which may not be necessary, if they've already been imported by another webpart, or in the master page).

Essential files all reside in the TEMPLATE\FEATURES\CodeConsoleWP folder, where:

CodeConsole.cs is the web part's source file. This emits all ui and javascript, including references to jQuery and JSON. Most essential of which is the following javascript. Yes, it's simple. Feel free to extend it. I'm sure intellisense would be a nice addition ;)

$(function(e){
    var input = $("#CodeConsole_input");
    var output = $("#CodeConsole_output");
    $("#CodeConsole_execute").click(function(be) {
        output.text("Compiling, please wait.");
        var code = {'code' : input.text()};
        var jsonStr = JSON.stringify(code);
        $.ajax({
            type: 'POST',
            url: '/_vti_bin/CodeConsoleSVC.asmx/ExecuteCode',
            data: jsonStr,
            contentType: 'application/json; charset=utf-8',
            dataType: 'json',
            success: function(msg) { output.text(msg.d); },
            error: function(xhr, msg) { output.text("Ajax Error:\n" + msg + "\n" + xhr.responseText); }
        });
    });
});


CodeConsoleSVC.cs is the web service source file, which basically looks like:

namespace CodeConsoleWP
{
    using System;
    using System.IO;
    using System.Web.Script.Services;
    using System.Web.Services;

    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    [ScriptService]
    public class CodeConsoleSVC : WebService
    {
        [WebMethod]
        public string ExecuteCode(string code)
        {
            using (TextWriter stringWriter = new StringWriter())
            {
                try
                {
                    Dynamic.ExecuteCode(code, stringWriter,
                                        new[] {"System""System.Text""System.IO""Microsoft.SharePoint"});
                    return stringWriter.ToString();
                }
                catch (Exception ex)
                {
                    return "Error:\n" + ex.Message;
                }
            }
        }
    }
}


Most interesting in the above web service, which will be deployed to the /_vti_bin/, along with other SharePoint services, is the call to Dynamic.ExecuteCode. This is a very simple class I typed up, which generates an assembly in-memory, referencing all assemblies of the hosting assembly (the web part, meaning SharePoint assemblies and such will be referenced), wrapping the code, executes it and returns an optional return value.

Dynamic.cs

internal static class Dynamic
{
    public static object ExecuteCode(string code, TextWriter output, string[] namespaces)
    {
        using (CodeDomProvider provider = CodeDomProvider.CreateProvider("C#"))
        {
            var parameters = new CompilerParameters
                                 {
                                     GenerateExecutable = false,
                                     IncludeDebugInformation = false,
                                     MainClass = "temp",
                                     GenerateInMemory = true
                                 };

            foreach (AssemblyName refasm in Assembly.GetExecutingAssembly().GetReferencedAssemblies())
            {
                parameters.ReferencedAssemblies.Add(Assembly.Load(refasm).Location);
            }
            string usingDirectives = "";
            foreach (string ns in namespaces)
            {
                usingDirectives += "using " + ns + ";";
            }
            CompilerResults results = provider.CompileAssemblyFromSource(parameters,
                                                                         @"namespace Dynamic {"
                                                                         + usingDirectives +
                                                                         @"public class Code { public object Run(System.IO.TextWriter output) { " + code + "; return null; } } }");
            if (results.Errors.Count > 0)
            {
                var sb = new StringBuilder();
                foreach (CompilerError error in results.Errors)
                {
                    sb.AppendLine(String.Format("{0},{1}: {2}", error.Line, error.Column, error.ErrorText));
                }
                throw new CompileException(sb.ToString());
            }
            else
            {
                object obj = results.CompiledAssembly.CreateInstance("Dynamic.Code");
                object ret = obj.GetType().InvokeMember("Run", BindingFlags.InvokeMethod, null, obj,
                                                        new object[] {output});
                return ret;
            }
        }
    }

    public class CompileException : Exception
    {
        public CompileException(string s)
            : base(s)
        {
        }
    }
}


Compiling and installing

Upon build, the source will be compiled, and put along with the service endpoint in a SharePoint solution file (wsp). This is taken care of by the build scripts in the DeploymentFiles folder. This file will end up in the CodeConsoleWP\wsp\ folder. To install the web part, just deploy this solution to the SharePoint server. Be sure to follow the instructions in my previous post, to add json/ajax support to SharePoint, though - otherwise the service call will fail miserably.

If you've got any questions, drop me a note in the comments here, on twitter, or by email.

Download Source Code + WSP