Wednesday, May 19, 2010

Silverlight 4.0 – A Simple SharePoint 2010 User Profile Browser

I have just started to play around with SharePoint 2010 and have found the social networking capabilities pretty slick. Just for fun, I decided to create a very simple Silverlight application that allows you browse different SharePoint user profiles. (Since I’m on a DEV environment, I decided to create a few test users from American history and one Japanese samurai actor. This application allows you to pick from a custom ComboBox that includes the personalized picture configured within SharePoint and display a few profile details like: location, skills, school, birthday, status, and about me. I will provide a full posting of the code after the sample screenshots below.

Honest Abe’s Profile Info

image

Toshiro Mifune (三船敏郎): In one of his classic poses from the movie Yojimbo (用心棒).
You will also notice that I created a tooltip of the user status when you mouse over items in the ComboBox.
image

Thomas Jefferson – Still waiting on that picture Tommy!

image

Steps
Note: I am assuming SharePoint 2010, Silverlight 4.0, and Visual Studio 2010, but with a little tweaking I’m sure you could do something similar with previous versions. I am not going to provide details explanation of everything I am doing

1. Enable Cross Domain Communication - Create a clientaccesspolicy.xml file in your SharePoint IIS root directory. You will need this in order to access SharePoint web services via Silverlight. You can find more information here on MSDN:

Making a Service Available Across Domain Boundaries

Mine was stored at C:\inetpub\wwwroot\wss\VirtualDirectories\80 and looks like the following:

<?xml version="1.0" encoding="utf-8"?>
<
access-policy>
<
cross-domain-access>
<
policy>
<
allow-from http-request-headers="*">
<
domain uri="*"/>
</
allow-from>
<
grant-to>
<
resource path="/" include-subpaths="true"/>
</
grant-to>
</
policy>
</
cross-domain-access>
</
access-policy>



2. Create a non GIF SharePoint Profile Picture

SharePoint uses the PERSON.GIF file stored in the IMAGES directory. Unfortunately, Silverlight doesn’t support the GIF format, so you need to open this up in Paint and save it back as a png or other supported format. You can find the PERSON.GIF file here:

C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\IMAGES

3. Create a WCF Service Application project in Visual Studio.

Note: I recommend creating a Silverlight Application called SharePointUserBrowser first, and then adding the WCF Project to it.

1.) Add a web service reference to the SharePoint UserGroup.asmx web service.
(Note: To add a web service reference in Visual Studio 2010: right-click on your project in Solution Explorer –> Click Add Service Reference…-> Click Advanced… –> Click Add Web Reference…)
Your SharePoint UserGroup web service should look something like this. I named mine “SPUserGroupService”.
http://portalname/_vti_bin/usergroup.asmx
2.) Add another web service reference to the SharePoint UserProfileService. I named mine “SPUserProfileService”.
http://portalname/_vti_bin/userprofileservice.asmx
3.) Add a UserInfo class to project. This will be used to populate the SharePoint user personalization information mapped from the PropertyData objects. (Note: This is only a subset of the information available. I have provided a full list at the bottom of this post.)

namespace SPUserBrowserService
{
public class UserInfo
{
public string Name { get; set; }
public string Email { get; set; }
public string PictureUrl { get; set; }
public string Status { get; set; }
public string AboutMe { get; set; }
public string Location { get; set; }
public string PastProjects { get; set; }
public string Skills { get; set; }
public string School { get; set; }
public string Birthday { get; set; }
public string Interests { get; set; }
}
}

4.) Add a WCF Service item to your project. I called mine SharePointService.
5.) In the following service contract to the interface:

using System.Collections.Generic;
using System.ServiceModel;
namespace SPUserBrowserService
{
[ServiceContract]
public interface ISharePointService
{
[OperationContract]
List<UserInfo> GetUserInfo();
}
}

6.) Add the following code to the service code behind.

using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Xml;
using System.Text.RegularExpressions;
using System.Xml.Linq;
using SPUserBrowserService.SPUserGroupService;
using SPUserBrowserService.SPUserProfileService;

namespace SPUserBrowserService
{
public class SharePointService : ISharePointService
{
public List<UserInfo> GetUserInfo()
{
UserGroup userGroup = new UserGroup();
userGroup.UseDefaultCredentials = true;
XmlNode allUsers = userGroup.GetAllUserCollectionFromWeb();
XNode xNode = XDocument.Parse(allUsers.OuterXml);
return (from root in xNode.Document.Elements()
from users in root.Elements()
from user in users.Elements()
let loginName = (string)user.Attribute("LoginName")
let userInfo = GetUserInfoDetails(loginName)
//Filter out admin accounts.
//Strangely enough I couldn’t find a consistent
//correlating property.
where loginName != "SHAREPOINT\\system" &&
loginName != "NT AUTHORITY\\LOCAL SERVICE"
select new UserInfo()
{
Name = loginName,
Email = userInfo.Email,
PictureUrl = userInfo.PictureUrl,
Status = userInfo.Status,
//Filter out HTML tags doesn't strip out HTML
//character entities
AboutMe = Regex.Replace(userInfo.AboutMe,
@"<(.\n)*?>", string.Empty),
Location = userInfo.Location,
PastProjects = userInfo.PastProjects,
Skills = userInfo.Skills,
School = userInfo.School,
Birthday = userInfo.Birthday,
Interests = userInfo.Interests
}).ToList();
}

private UserInfo GetUserInfoDetails(string loginName)
{
//Set the default picture URL. Silverlight can't use the default GIF
//provided by SharePoint.
UserInfo userInfo = new UserInfo() {
PictureUrl = @"http://portalname/_layouts/images/person.png" };
try
{
UserProfileService userProfileService = new UserProfileService();
userProfileService.UseDefaultCredentials = true;
PropertyData[] data = userProfileService.GetUserProfileByName(loginName);
string pictureUrl = GetPropertyData(data, "PictureURL");
//This is empty be default until the user picks a picture.
if (!string.IsNullOrEmpty(pictureUrl))
{
userInfo.PictureUrl = pictureUrl;
}
userInfo.Status = GetPropertyData(data, "SPS-StatusNotes");
userInfo.AboutMe = GetPropertyData(data, "AboutMe");
userInfo.Email= GetPropertyData(data, "WorkEmail");
userInfo.Location = GetPropertyData(data, "SPS-Location");
userInfo.PastProjects = GetPropertyData(data, "SPS-PastProjects");
userInfo.Skills = GetPropertyData(data, "SPS-Skills");
userInfo.School = GetPropertyData(data, "SPS-School");
userInfo.Birthday = GetPropertyData(data, "SPS-Birthday");
userInfo.Interests = GetPropertyData(data, "SPS-Interests");
}
catch (System.Web.Services.Protocols.SoapException ex)
{
//It appears that an exception gets generated for users
//that have not logged into SharePoint yet.
//They must logon for a profile to be created.
if (!ex.Message.Contains("A user with the account name"))
{
throw;
}
}
return userInfo;
}

private string GetPropertyData(PropertyData[] data, string columnName)
{
//Delimit properties with a semi-colon or comma.
return string.Join("; ",
(from v in
((PropertyData)data.Where(x => x.Name == columnName).Single()).Values
select v.Value.ToString()).ToArray());
}
}
}

7.) VERY IMPORTANT: Add a clientaccesspolicy.xml file to your WCF Service application. If you don’t you will receive a CommunicationException exception. You can copy this from the one you created in Step 1.

Note: You will see that I am basically pulling everyone. You can do further filtering, by specifying users in a specific team site or SharePoint group. You will also see that I have to call the UserProfileService web service to return the details for each user. This could be potentially expensive with a ton of users, so you may need to provide other mechanisms for filtering. It would be nice if there were a way to retrieve a collection of UserProfile objects, but I haven’t found a way in the SharePoint web services or object model. I’m sure you could pull it from the database, but of course Microsoft doesn’t support this. If you’re feeling brave or rebellious, see my post on this:

SharePoint 2007 – FIVE very helpful (yet unsupported) stored procedures for querying membership data

At this point, you are ready to move on to the Silverlight implementation. However, I would highly recommend testing the service with something like a simple console application to make sure everything is property working.

4. Create a Silverlight Project
1.) If you haven’t already, create a Silverlight Project. I called mine SPUserBrowser.
2.) Add a service reference to the WCF Service you created in the previous step. I called mine SPUserBrowserService.
3.) Update the XAML in MainPage.xaml to the following:

<UserControl x:Class="SPUserBrowser.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:SPUserBrowser"
mc:Ignorable="d" d:DesignWidth="550" MaxWidth="475" MaxHeight="370" d:DesignHeight="370" FontFamily="Calibri" FontSize="13" Margin="10">
<
UserControl.Resources>
<
Style x:Name="Prompt" TargetType="TextBlock">
<
Setter Property="FontWeight" Value="Bold" />
<
Setter Property="Margin" Value="3,3" />
<
Setter Property="Width" Value="70" />
<
Setter Property="HorizontalAlignment" Value="Left"></Setter>
<
Setter Property="FontStyle" Value="Italic" />
</
Style>
<
Style x:Name="InfoContent" TargetType="TextBlock">
<
Setter Property="HorizontalAlignment" Value="Left"></Setter>
<
Setter Property="TextTrimming" Value="WordEllipsis" />
<
Setter Property="Foreground" Value="Black" />
</
Style>
<
Style x:Name="Status" TargetType="TextBlock">
<
Setter Property="FontWeight" Value="Normal" />
<
Setter Property="FontSize" Value="14" />
<
Setter Property="TextWrapping" Value="Wrap" />
<
Setter Property="FontStyle" Value="Italic" />
</
Style>
<
Style x:Name="BorderWhiteSmoke" TargetType="Border">
<
Setter Property="CornerRadius" Value="5" />
<
Setter Property="Background" Value="WhiteSmoke" />
<
Setter Property="Margin" Value="4,4" />
</
Style>
<
local:DateConverter x:Key="dateConverter" />
</
UserControl.Resources>
<
Grid>
<
Border CornerRadius="20">
<
Border.Background>
<
LinearGradientBrush StartPoint="0,0" EndPoint="0.3,1">
<
GradientStop Color="#FFC1B2B2" Offset="0"/>
<
GradientStop Color="#FF424949" Offset="0.5"/>
<
GradientStop Color="#FF2E1F1F" Offset="1"/>
<
GradientStop Color="#FF5F5050" Offset="0.246"/>
</
LinearGradientBrush>
</
Border.Background>
<
Grid x:Name="LayoutRoot" VerticalAlignment="Top" Background="Transparent" Margin="5,5">
<
Grid.RowDefinitions>
<
RowDefinition Height="25"></RowDefinition>
<
RowDefinition Height="*"></RowDefinition>
<
RowDefinition Height="50"></RowDefinition>
</
Grid.RowDefinitions>
<
TextBlock Text="SharePoint User Browser" FontSize="16" FontWeight="Bold" Foreground="WhiteSmoke" Margin="3,3"></TextBlock>
<
Grid x:Name="ProfileGrid" Background="Transparent" Grid.Row="1" Margin="8">
<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="100"></ColumnDefinition>
<
ColumnDefinition></ColumnDefinition>
</
Grid.ColumnDefinitions>
<
Grid.RowDefinitions>
<
RowDefinition Height="32"></RowDefinition>
<
RowDefinition Height="32"></RowDefinition>
<
RowDefinition Height="32"></RowDefinition>
<
RowDefinition Height="32"></RowDefinition>
<
RowDefinition Height="32"></RowDefinition>
<
RowDefinition Height="95"></RowDefinition>
</
Grid.RowDefinitions>
<
Border Grid.Column="0" Grid.Row="0" Grid.RowSpan="4" CornerRadius="15" Background="AliceBlue" Margin="0">
<
Image x:Name="UserImage" Source="{Binding PictureUrl}" Margin="5,5" Height="110" ></Image>
</
Border>
<
Border Grid.Row="0" Grid.Column="1" Style="{StaticResource BorderWhiteSmoke}">
<
Grid>
<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="60"></ColumnDefinition>
<
ColumnDefinition></ColumnDefinition>
</
Grid.ColumnDefinitions>
<
TextBlock x:Name="LocationPrompt" Text="Location:" Style="{StaticResource Prompt}" Grid.Column="0" Grid.Row="0" />
<
TextBlock x:Name="Location" Text="{Binding Location}" ToolTipService.ToolTip="{Binding Location}" Style="{StaticResource InfoContent}" Grid.Column="1" Grid.Row="0" />
</
Grid>
</
Border>
<
Border Grid.Row="1" Grid.Column="1" Style="{StaticResource BorderWhiteSmoke}">
<
Grid>
<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="60"></ColumnDefinition>
<
ColumnDefinition></ColumnDefinition>
</
Grid.ColumnDefinitions>
<
TextBlock x:Name="SkillsPrompt" Text="Skills:" Style="{StaticResource Prompt}" Grid.Column="0" Grid.Row="0" />
<
TextBlock x:Name="Skills" Text="{Binding Skills}" ToolTipService.ToolTip="{Binding Skills}" Style="{StaticResource InfoContent}" Grid.Column="1" Grid.Row="0" />
</
Grid>
</
Border>
<
Border Grid.Row="2" Grid.Column="1" Style="{StaticResource BorderWhiteSmoke}">
<
Grid>
<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="60"></ColumnDefinition>
<
ColumnDefinition></ColumnDefinition>
</
Grid.ColumnDefinitions>
<
TextBlock x:Name="SchoolPrompt" Text="School:" Style="{StaticResource Prompt}" Grid.Column="0" Grid.Row="0" />
<
TextBlock x:Name="School" Text="{Binding School}" ToolTipService.ToolTip="{Binding School}" Style="{StaticResource InfoContent}" Grid.Column="1" Grid.Row="0" />
</
Grid>
</
Border>
<
Border Grid.Row="3" Grid.Column="1" CornerRadius="5" Style="{StaticResource BorderWhiteSmoke}">
<
Grid>
<
Grid.ColumnDefinitions>
<
ColumnDefinition Width="60"></ColumnDefinition>
<
ColumnDefinition></ColumnDefinition>
</
Grid.ColumnDefinitions>
<
TextBlock x:Name="BirthdayPrompt" Text="Birthday:" Style="{StaticResource Prompt}" Grid.Column="0" Grid.Row="0" />
<
TextBlock x:Name="Birthday" Text="{Binding Birthday, Converter={StaticResource dateConverter}}" Style="{StaticResource InfoContent}" Grid.Column="1" Grid.Row="0" />
</
Grid>
</
Border>
<
TextBlock Text="{Binding Status}" Foreground="LightGreen" ToolTipService.ToolTip="{Binding Status}" VerticalAlignment="Center" Grid.Column="0" Grid.Row="4" Grid.ColumnSpan="3" FontSize="18" FontStyle="Italic" TextTrimming="WordEllipsis"></TextBlock>
<
Border Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="3" Background="WhiteSmoke" CornerRadius="15" Margin="3">
<
StackPanel VerticalAlignment="Top" Orientation="Vertical" Margin="5">
<
TextBlock Text="About Me" Height="15" Style="{StaticResource Prompt}"></TextBlock>
<
TextBox x:Name="AboutMe" Height="55" Text="{Binding AboutMe}" TextWrapping="Wrap" IsReadOnly="True" VerticalScrollBarVisibility="Visible" HorizontalScrollBarVisibility="Hidden" BorderThickness="0" />
</
StackPanel>
</
Border>
</
Grid>
<
StackPanel Orientation="Horizontal" Grid.Row="2" Margin="10,0,10,10">
<
TextBlock Text="Pick a user:" Width="80" Foreground="LightBlue" Style="{StaticResource Prompt}" FontSize="16" VerticalAlignment="Center" Margin="3,3" />
<
ComboBox x:Name="PictureComboBox" Width="360" Height="40" SelectionChanged="PictureComboBox_SelectionChanged">
<
ComboBox.ItemTemplate>
<
DataTemplate>
<
StackPanel Orientation="Horizontal" VerticalAlignment="Center" ToolTipService.ToolTip="{Binding Status}">
<
Image Source="{Binding PictureUrl}" Width="40" />
<
TextBlock Text="{Binding Name}" Foreground="Black" FontSize="16" VerticalAlignment="Center" Margin="5,5" />
</
StackPanel>
</
DataTemplate>
</
ComboBox.ItemTemplate>
</
ComboBox>
</
StackPanel>
</
Grid>
</
Border>
</
Grid>
</
UserControl>







4.) Add the following code to your MainPage.xaml code behind:

using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
using SPUserBrowser.SPUserBrowserService;
namespace SPUserBrowser
{
public partial class MainPage : UserControl
{
public MainPage()
{
InitializeComponent();
SharePointServiceClient c = new SharePointServiceClient();
c.GetUserInfoCompleted += new EventHandler<GetUserInfoCompletedEventArgs>(c_GetUserInfoCompleted);
c.GetUserInfoAsync();
}

void c_GetUserInfoCompleted(object sender, GetUserInfoCompletedEventArgs e)
{
PictureComboBox.ItemsSource = e.Result;
PictureComboBox.SelectedIndex = 0;
ProfileGrid.DataContext = e.Result[0];
}

private void PictureComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
UserInfo userInfo = (UserInfo)PictureComboBox.SelectedItem;
ProfileGrid.DataContext = userInfo;
}
}

public class DateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
string returnValue = string.Empty;
if (!string.IsNullOrEmpty(value.ToString()))
{
DateTime date = DateTime.Parse(value.ToString());
returnValue = CultureInfo.CurrentCulture.DateTimeFormat.GetMonthName(date.Month) + " " + date.Day.ToString();
}
return returnValue;
}

public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}

That’s basically it! You should be good to go.

Other Information

PropertyData List
Here is a list of PropertyData names that are used in the SharePoint UserProfileService web service:

UserProfile_GUID
AccountName
FirstName
SPS-PhoneticFirstName
LastName
SPS-PhoneticLastName
PreferredName
SPS-PhoneticDisplayName
WorkPhone
Department
Title
SPS-JobTitle
Manager
AboutMe
PersonalSpace
PictureURL
UserName
QuickLinks
WebSite
PublicSiteRedirect
SPS-Dotted-line
SPS-Peers
SPS-Responsibility
SPS-SipAddress
SPS-MySiteUpgrade
SPS-ProxyAddresses
SPS-HireDate
SPS-DisplayOrder
SPS-ClaimID
SPS-ClaimProviderID
SPS-ClaimProviderType
SPS-SavedAccountName
SPS-ResourceAccountName
SPS-ObjectExists
SPS-MasterAccountName
SPS-DistinguishedName
SPS-SourceObjectDN
WorkEmail
CellPhone
Fax
Office
SPS-Location
SPS-TimeZone
Assistant
SPS-PastProjects
SPS-Skills
SPS-School
SPS-Birthday
SPS-StatusNotes
SPS-Interests
SPS-EmailOptin

SharePoint 2010 Web Services List
You can browse them easily by adding _vti_bin/webservicename.asmx to any portal site. These are found in the following directory:
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI

image

11 comments:

  1. Pretty cool. What made you choose the web services API over the client OM?

    ReplyDelete
  2. Oh wow! I just looked this up and wasn't aware of this library. I just started playing with 2010, so I'll definitely update the code. I've always preferred the object model, but thought I needed web services as I was accessing it remotely.

    ReplyDelete
  3. Apparently, it doesn't look like user profile information is available via the client object model. The closest object I can find is User and Utilities.PrincipalInfo, but both only include basic properties like Name, Email, and JobTitle. Please correct me if I'm wrong. I would definitely prefer to use an object model.

    ReplyDelete
  4. Why didn't go add the usergroup.asmx and userprofileservice.asmx services straight to the Silverlight project (as opposed to first adding them to WCF project, then adding WCF service to Silverlight)?

    ReplyDelete
  5. Hi John, Same problem here with the Client Object model and User profiles. I am fairly new to Sharepoint / C# development and after a week of trying all sorts of methodes to get a simple profile picture URL I used your Service app and that did the trick. Thanks very much!!!!

    ReplyDelete
  6. One question.
    How can you have other users profile with GetUserProfileByName? You are using Default credentials ( userProfileService.UseDefaultCredentials = true;) so i think it uses the browser crendentials wich mean that are connected with an account. Is it a simple user account or an admin account?
    I'm trying to get users accounts like you did but using my own user credentials I can only get my profile.

    ReplyDelete
  7. You will definitely want to use some kind of account with enough permissions. In my case, I was on a DEV machine using a farm admin account.

    ReplyDelete
  8. Its good to see this article. But I need a small tweak in the existing OOB web part. Here by default it displays the Name and Title in the Silverlight Organization webpart. I need to show one more property like Email or Location. Could you suggest me on this.

    Thanks
    Dinesh

    ReplyDelete
  9. Hi,If you are new to web page design then I would highly recommend you start out with an HTML editor in Web Design Cochin that provides step by step process to build your web page. The reason for this is quite simple, build a web page from scratch can be quite a daunting task, Thanks.....

    ReplyDelete
  10. Great Article very helpul
    Eliteonrent We provide Air Conditioning rental services

    ReplyDelete