Building a Windows Sidebar Gadget with WPF against SPS 2010
I've been working on creating a solution for the internal MS sales teams here that has some really interesting components/code that I thought I would share. Basically, I was asked to create a windows sidebar gadget that displayed data that was maintained in some SharePoint 2010 lists. There were of course a few twists. The gadget had to continue to work while the sales folks were offline and where possible, I needed to incorporate photo's from people's my sites. I chose to build the solution in WPF. This was after someone else had made an attempt with Silverlight only to realize that on a 64bit machine, Silverlight sidebar gadgets are not supported since the sidebar is run by IE 64bit. So the only real choices were WPF and Javascript. I have done Javascript in the past, but was interested in the challenge of a WPF solution. Here are a few of the major code elements.
Setup of the Solution
The first issue is just really the setup and configuration of how to get a WPF project to render as a gadget. A few tricks I picked up. First of all the main project in the solution was creating using the WPF Browser Application Template. I then added a second project to the solution that I called GadgetDeployment. This second project has the gadget.xml file, a default.html that points to the xbap file created by the WPF app. The trick is to use the Publish Now click-once functionality to place the xbap file and the other application files into the deployment project. Then all you have to do is zip everything up there so that the gadget.xml file is in the root and change the extention from .zip to .gadget. Here are a few code snippets to help you out with some of the details.
First, the gadget.xml file (notice the full security permission since the gadget will have to call some web services):
<?xml version="1.0" encoding="utf-8" ?>
<gadget>
<name>StuFinder WPF</name>
<namespace>edhild</namespace>
<version>1.0</version>
<author name="Ed Hild">
<info url="https://blogs.msdn.com/edhild" />
</author>
<copyright>2010</copyright>
<description>StuFinder XBAP Gadget</description>
<hosts>
<host name="sidebar">
<base type="HTML" apiVersion="1.0.0" src="default.html" />
<permissions>full</permissions>
<platform minPlatformVersion="0.3" />
</host>
</hosts>
</gadget>
And then the default.html file which delivers the WPF browser application:
<html>
<head>
<title>StuFinder - WPF</title>
<style>
body {
width:234px;
height:273px;
padding:1;
margin:0;
background:gray;
}
</style>
</head>
<body>
<iframe height="273px"
width="234px"
src="StuFinder.xbap" />
</body>
</html>
So after you have done a click-once publish of the application, the results should look like the picture below. You only need to include the Application Files folder, the html file, the gadget xml file, and the xbap file in the deployed gadget. Note that if you actually publish more than once the Application Files folder will get subfolders for each version. You can delete these so you are only deploying the most current version.
Querying the SharePoint Site and use of Isolated Storage
So I had a few SharePoint lists that I needed to pull down into the gadget. I made the choice to use the client-side API for SharePoint 2010 to query for the items. I then created my own (cleaner) XML representation of those items that I stored in isolated storage. Since there were three lists, this resulted in 3 XML files in isolated storage. I'm not doing anything fancy like only looking for updated items on a sync. Since there isn't a tremendous amout of data, I just retrieve all of the items. Here are some code snippets for one of the lists. By the way, I am doing all of this at the App object level of the solution.
public partial class App : Application
{
public const string STUSPECIALTIES_ISOLATED_FILE_NAME = "STUSpecialties.xml";
public const string SITEURL = "https://sharepoint/siteurl";
public static XmlDataDocument StuSpecialties = null;
public static IsolatedStorageFile isoStore = null;
public static void InitData()
{
//try to retrieve from storage
isoStore = IsolatedStorageFile.GetStore(IsolatedStorageScope.User | IsolatedStorageScope.Assembly, null, null);
EstablishStuSpecialtiesData();
}
Here we see if there is already a file in isolated storage and if so retrieve it. Otherwise, start querying the SharePoint site.
private static void EstablishStuSpecialtiesData()
{
string[] fileNames = isoStore.GetFileNames(STUSPECIALTIES_ISOLATED_FILE_NAME);
bool isFound = false;
foreach (string file in fileNames)
{
if (file == STUSPECIALTIES_ISOLATED_FILE_NAME)
{
isFound = true;
}
}
if (isFound)
{
//read from Isolated Storage
IsolatedStorageFileStream iStream = new IsolatedStorageFileStream(STUSPECIALTIES_ISOLATED_FILE_NAME, System.IO.FileMode.Open, isoStore);
StuSpecialties = new XmlDataDocument();
StuSpecialties.Load(iStream);
}
else
{
RetrieveStuSpecialties();
}
}
And then here is the actual query part where I am using the client-side API.
public static void RetrieveStuSpecialties()
{
//if not in storage - query SharePoint site
bool success = QueryStuSpecialties();
//and then store in isolated storage
if (success)
{
IsolatedStorageFileStream oStream = new IsolatedStorageFileStream(STUSPECIALTIES_ISOLATED_FILE_NAME, System.IO.FileMode.Create, isoStore);
XmlTextWriter writer = new XmlTextWriter(oStream, Encoding.UTF8);
writer.WriteRaw(StuSpecialties.OuterXml);
writer.Close();
}
}
private static bool QueryStuSpecialties()
{
using (SPClient.ClientContext ctx = new SPClient.ClientContext(SITEURL))
{
//query to get the list items
SPClient.Web web = ctx.Web;
SPClient.ListCollection coll = web.Lists;
SPClient.List list = coll.GetByTitle("STU Info Specialties");
SPClient.CamlQuery query = SPClient.CamlQuery.CreateAllItemsQuery();
SPClient.ListItemCollection listItems = list.GetItems(query);
ctx.Load(
listItems,
items => items
.Include(
item => item["ID"],
item => item["Title"]));
try
{
ctx.ExecuteQuery();
//build XML representation of the results for storage and use
StuSpecialties = new System.Xml.XmlDataDocument();
XmlNode xml = StuSpecialties.CreateNode(XmlNodeType.XmlDeclaration, string.Empty, string.Empty);
StuSpecialties.AppendChild(xml);
//add the root
XmlNode rootNode = StuSpecialties.CreateElement("items");
StuSpecialties.AppendChild(rootNode);
foreach (SPClient.ListItem item in listItems)
{
XmlElement itemNode = StuSpecialties.CreateElement("item");
itemNode.AppendChild(CreateElementHelper("id", "ID", StuSpecialties, item));
itemNode.AppendChild(CreateElementHelper("title", "Title", StuSpecialties, item));
//add item to the root
rootNode.AppendChild(itemNode);
}
return true;
}
catch
{
MessageBox.Show( "Cannot retrieve Specialties");
return false;
}
}
Getting Photo's from My Sites
One other neat trick is that one of my lists references data about resources and I wanted to include the pictures of those user's from their my sites. Now these pictures will only show up if the user is online and has line of sight to the SharePoint server. I am not storing the images in isolated storage. I only appended the Urls to those images in my XML file. So during the processing of pulling down the data, I use the user's account name: DOMAIN\USER to get the my site picture url property. This code snippet is here as well for your enjoyment. Just remember that you would need to add a reference to the User Profile Web Service for this code to work.
private static XmlElement GetImageInfo(XmlDataDocument doc, SPClient.ListItem item)
{
XmlElement elementNode = doc.CreateElement("pictureurl");
XmlText elementText = null;
string unknownUrl = "https://msw/_layouts/images/o14_person_placeholder_96.png";
string accountName = item["MSDomain"].ToString() + "\\" + item["Title"].ToString();
//setup the picture
try
{
UserProfileSve.UserProfileServiceSoapClient client = new UserProfileSve.UserProfileServiceSoapClient();
client.ClientCredentials.Windows.AllowedImpersonationLevel = System.Security.Principal.TokenImpersonationLevel.Impersonation;
UserProfileSve.PropertyData property = client.GetUserPropertyByAccountName(accountName, "PictureURL");
elementText = doc.CreateTextNode(property.Values[0].Value.ToString());
}
catch
{
elementText = doc.CreateTextNode(unknownUrl);
}
elementNode.AppendChild(elementText);
return elementNode;
}
There is also another nice helper method I created for parsing some of the SharePoint item properties for building my own XML representation of them. I thought someone might want to reuse it.
private static XmlElement CreateElementHelper(string elementName, string fieldName, XmlDataDocument doc, SPClient.ListItem item)
{
XmlElement elementNode = doc.CreateElement(elementName);
XmlText elementText = null;
if (item[fieldName] is SPClient.FieldUrlValue)
{
//email field
SPClient.FieldUrlValue urlField = (SPClient.FieldUrlValue)item[fieldName];
string urlAddr = urlField.Description;
elementText = doc.CreateTextNode(urlAddr);
}
else if (item[fieldName] is SPClient.FieldLookupValue)
{
SPClient.FieldLookupValue lookupField = (SPClient.FieldLookupValue)item[fieldName];
string lookupValue = lookupField.LookupValue;
elementText = doc.CreateTextNode(lookupValue);
}
else
{
elementText = doc.CreateTextNode(item[fieldName].ToString());
}
elementNode.AppendChild(elementText);
return elementNode;
}
Comments
Anonymous
June 27, 2010
Hello. Nice solution. Have you tried to make it run under Windows classic theme? My assumption is that you are using Windows 7. I have that kind of problem with my WPF (.xbap) gadget. Also there is 'gadget size problem' after you change font-size (and items size) on windows level (Personalization -> Display -> 125%, for example). Regards, NenadAnonymous
June 28, 2010
true - I have seen the problem with the font-size in Windows 7. I found that some fonts are more freindly than others and I've coded not to a fixed width to help. It isn't ideal, but the other benefits are worth the tradeoff.