Saturday, December 19, 2009

WorkflowInvoker in WF 4.0

Just noticed this new and handy class in .NET 4.0 Beta 2. Following is its description on MSDN document:

"Windows Workflow Foundation (WF) provides several methods of hosting workflows. WorkflowInvoker provides a simple way for invoking a workflow as if it were a method call and can be used only for workflows that do not use persistence."

Simple enough, if we just want to invoke a workflow synchronously with current thread (WorkflowInvoker also provides asynchronous versions of the invoke method with InvokeAsync and BeginInvoke), WorkflowInvoker is your best friend, and you don't need to set up the environment for workflow runtime:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Activities;
using System.Activities.Statements;

class Program
{
static void Main(string[] args)
{
Activity activity = new WriteLine() { Text = "Workflow running at " + DateTime.Now.ToString() };
WorkflowInvoker.Invoke(activity);
Console.ReadLine();
}
}
For long running workflows or persistence scenarios, .NET 4.0 also added a new WorkflowApplication class, which provides a richer model for executing workflows that includes notification of lifecycle events, execution control, bookmark resumption, and persistence. Details refer to http://msdn.microsoft.com/en-us/library/system.activities.workflowapplication.aspx.

Friday, December 11, 2009

New DynamicObject In .NET 4.0

Microsoft has released SharePoint 2010 Beta2 and .NET 4.0 Beta2 recently (Ironically SharePoint 2010 is still based on .NET 3.5).

The new Dynamic type in .NET 4.0 looks quite interesting. Not like .NET 3.5 dynamic variable (var keyword) which is static type inference by compiler, Dynamic objects are run-time behavior. Following code example is copied from MSDN documentation, and comments are removed for brevity reason:
using System;
using System.Collections.Generic;
using System.Dynamic;

public class DynamicDictionary : DynamicObject
{
Dictionary<string, object> dictionary = new Dictionary<string, object>();
public int Count { get { return dictionary.Count; }}

public override bool TryGetMember(GetMemberBinder binder, out object result)
{
string name = binder.Name.ToLower();
return dictionary.TryGetValue(name, out result);
}

public override bool TrySetMember(SetMemberBinder binder, object value)
{
dictionary[binder.Name.ToLower()] = value;
return true;
}
}

class Program
{
static void Main(string[] args)
{
dynamic person = new DynamicDictionary();
person.FirstName = "Ellen";
person.LastName = "Adams";

Console.WriteLine(person.firstname + " " + person.lastname);
Console.WriteLine( "Number of dynamic properties:" + person.Count);
Console.Read();
}
}
The result is:
Ellen Adams
Number of dynamic properties:2

Thursday, December 03, 2009

SharePoint Impersonation by SPUser

The SPUser class has a UserToken property that can be passing into the SPSite constructor to impersonate that particular user:
        SPSite contextSite = SPContext.Current.Site;
        SPUser user = contextSite.SystemAccount;
        using (SPSite site = new SPSite(contextSite.ID, user.UserToken))
        {
            using (SPWeb web = site.OpenWeb())
            {
                // Do stuff
            }
        }
Above code snippet impersonates the system account to open a SPSite which is equivalent to:
        SPSecurity.RunWithElevatedPrivileges(delegate()
        {
            using (SPSite site = new SPSite(SPContext.Current.Site.ID))
            {
                using (SPWeb web = site.OpenWeb())
                {
                    // Do stuff
                }
            }
        });

Tracing IIS 500 Error Using Failed Request Tracing Rules

After extended a SharePoint Web Application I got a 500 server error. There's not related event log and the IIS log shows:
2009-12-02 19:37:21 IP GET / - 10.10.1.11 Jakarta+Commons-HttpClient/3.1 500 19 183 0
The HTTP status code 500.19 for IIS 7.0 is "Configuration data is Invalid.". That's not very helpful and I couldn't find obvious issue in web.config. To see more detailed error message I enabled IIS Failed Request Tracing, where I was able to find out the exact error in configuration file:



So the error is caused by "cannot add duplicate collection entry of type 'add' with unique key attribute 'name' set to 'session'". It looks like the "Session" module has already been registered somewhere by SharePoint when the configuration entries are merged. Change the the web.config from original:
    <add name="Session" type="System.Web.SessionState.SessionStateModule" />
    <remove name="Session" />
to:
    <remove name="Session" />
    <add name="Session" type="System.Web.SessionState.SessionStateModule" />
Then the extended web application works normally.

Saturday, November 28, 2009

Word 2007 Document Processing Using OpenXML

One interesting topic is how to handle Word documents using code. I did a test to export the page content from a page in publishing site's Pages library to a Word 2007 document, and save it to a separate document library with success.

Code:
   /// <summary>
/// Export publishing page's content to Word 2007 document controls
/// Exported documents stored in a separate document library
/// </summary>
/// <param name="sourceItem">A list item from Pages' library</param>
/// <param name="targetList">A document library saves exported Word 2007 documents</param>
public static void ExportPubPageContentToWordDoc(SPListItem sourceItem, SPList targetList)
{
SPDocumentLibrary lib = targetList as SPDocumentLibrary;
if (lib == null)
{
throw new Exception("Target list is not a Document Library type");
}

foreach (SPContentType ctype in lib.ContentTypes)
{
if (ctype.Name.ToLower() != "document" && ctype.Name.ToLower() != "folder")
{
SPFile tempFile = ctype.ResourceFolder.Files[ctype.DocumentTemplate];
using (Stream fileStream = tempFile.OpenBinaryStream())
{
BinaryReader reader = new BinaryReader(fileStream);
MemoryStream memString = new MemoryStream();
BinaryWriter writer = new BinaryWriter(memString);
writer.Write(reader.ReadBytes((int)fileStream.Length));
writer.Flush();
reader.Close();

using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(memString, true))
{
MainDocumentPart mainPart = wordDoc.MainDocumentPart;
IEnumerator<CustomXmlPart> xmlPartEnumerator = mainPart.CustomXmlParts.GetEnumerator();
xmlPartEnumerator.MoveNext();
CustomXmlPart XMLPart = xmlPartEnumerator.Current;

// Create an XML document that matches our structure
XmlDocument doc = new XmlDocument();

// Create some nodes
XmlElement rootNode = doc.CreateElement("propertydata");
XmlElement titleNode = doc.CreateElement("title");
XmlElement body = doc.CreateElement("body");

titleNode.InnerText = GetFieldValueString(sourceItem, "Title");
rootNode.AppendChild(titleNode);
doc.AppendChild(rootNode);

body.InnerText = GetFieldValueString(sourceItem, "Article Body");
rootNode.AppendChild(body);
doc.AppendChild(rootNode);

MemoryStream resultStream = new MemoryStream();
doc.Save(resultStream);
resultStream.Flush();
resultStream.Position = 0;
XMLPart.FeedData(resultStream);

string fileName = sourceItem.File.Name;
if (fileName.IndexOf('.') > 0)
fileName = fileName.Substring(0, fileName.LastIndexOf('.'));
fileName += ".docx";
string docUrl = lib.RootFolder.Url + "/" + fileName;
SPFile newDoc = lib.RootFolder.Files.Add(docUrl, memString, true);
lib.Update();
}
}
}
}
}
OpenXML SKD 2.0 (http://www.microsoft.com/downloads/details.aspx?FamilyId=C6E744E5-36E9-45F5-8D8C-331DF206E0D0&displaylang=en) is required to run above code. Word 2007 Content Control tool-kit (http://dbe.codeplex.com/) is handy to manipulate Word 2007 documents'XML, and I used it to create the document library template file.

Good references on this topic:
http://blogs.msdn.com/mikeormond/archive/2008/06/20/word-2007-content-controls-databinding-and-schema-validation.aspx
http://www.craigmurphy.com/blog/?p=913
http://www.microsoft.com/uk/msdn/screencasts/screencast/236/Word-2007-Content-Controls-and-Schema-Validation.aspx

Sunday, November 22, 2009

SharePoint Content Type And Word Template

One nice feature in SharePoint is that the SharePoint content type and its Word template can be cooperating together. The content type field values could be treated as metadata and is injected to its Word 2007 document template as document properties. These document properties can be viewed in Word’s information panel (enable it by Word 2007 setting Prepare->Properties). User can update these properties directly in information panel and upload the Word document back to SharePoint Document Library. The SharePoint list item’s corresponding fields will be updated automatically. The steps are:
  1. Create an empty Word 2007 template.
  2. Create a new Content Type by “Site Actions > Site Settings > Site Content Types > Create”, select “Document Content types – Document” as parent.
  3. Add required fields to the new Content Type.
  4. Upload the Word 2007 template by “Advanced settings > Upload a new document template”.
  5. Create a new Document Library and enable the content type management by “Settings > Advanced settings > Allow management of content types? > Yes”.
  6. Add content type created in step 2 to the document library created in step 5.
  7. Add new document library item by selecting the template created in step 1.
If you don’t like working with Word information panel, you have option to use the Word content controls inside the Word document body to do similar things, and sync the metadata back to SharePoint. We can define the word template in our desire and associate document properties to Word document content controls. Following screen-shot illustrates how document properties can be tied to Word content controls inside Word document, note that Title, Title_fr, Sub_Title, Sub_Title_fr in the example are the Content Type fields inside SharePoint Document Library:


We can also create Word 2007 content controls under developer tab (Ribbon), but there is no direct association between the content controls and document properties if we do so. OpenXML and Word 2007 custom properties techniques are required for Word content automation (SharePoint/Word data binding). I will put more details about this in my next post.

Although SharePoint can generate document properties automatically, not all SharePoint fields are supported by document properties; and not all types of document property are supported by Word content controls. Following table lists mapping of common SharePoint fields and Word 2007 content control:



SharePoint Field Type

Word 2007 Content Control

Single line of text

Text (Not allow carriage returns)

Multiple lines of text

Text (Allow carriage returns)

Choice

Dropdown list

Number

Text with Schema validation

Currency

Text with Schema validation

Date and Time

Date picker

Yes/No

Dropdown list

Lookup

N/A

Person or Group

N/A

Hyperlink or Picture

N/A

Calculated

N/A

Custom filed

N/A



For those SharePoint fields missing World 2007 equivalent content control, we could create a Word content control compatible field in content type, and manually convert the original SharePoint field to that compatible field and versa vise inside list item event receiver. For example, a Text field can be used to map a custom field which is not recognized by Word 2007.

Tuesday, October 27, 2009

Update SharePoint List Column Property Programatically

In order to update a list column (field) property in SharePoint, such as the setting of required or not, we can do the change through the column's setting page from the list's setting page. But some field properties don't display in setting page, and you can't make the change via UI.

Can we update a field property programatically? The answer is yes. For those field properties exposed directly to SPField, the update is simple. Following code sets a list column named "CustomField" to be hidden:
        using (SPSite site = new SPSite("http://localhost"))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists["MyCustomList"];
list.Fields["CustomField"].Hidden = true;
list.Update();
}
}
If the field property is not exposed to a SPField object, we can also make the change by updating the field's schema XML. SPField has a property called "ShowInNewForm" that determines whether the field shows in the NewForm page (the page to create a new list item). Following code demos how to set a site column not to display in NewForm page:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

class Program
{
static void Main(string[] args)
{
TurnOffFieldInNewForm("http://localhost", "My Custom List", "CustomField");
}


static void TurnOffFieldInNewForm(string siteName, string listName, string fieldName)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb("Team"))
{
SPList list = web.Lists[listName];
string origSchemaXml = web.Fields.GetFieldByInternalName(fieldName).SchemaXml;

string schemaXml = origSchemaXml.Replace("ShowInNewForm=\"TRUE\"", "ShowInNewForm=\"FALSE\"");
if (!schemaXml.Contains("ShowInNewForm="))
{
int index = schemaXml.IndexOf("></Field>");
schemaXml = schemaXml.Substring(0, index) + " ShowInNewForm=\"FALSE\"></Field>";
}

web.Fields.GetFieldByInternalName(fieldName).SchemaXml = schemaXml;
list.Fields.GetFieldByInternalName(fieldName).SchemaXml = schemaXml;
list.Update();
web.Update();
}
}
}
}

Note: updating the site column property doesn't have impact on existing lists that are using that site column, and the update only takes effect for new lists or new referencing to that site column. Because the list column copies the site column's schema xml once when it's first created. So we need to update the list column's property in existing lists separately.

Wednesday, October 14, 2009

JQuery Auto-complete Based On SPList Items In SharePoint Environment

Step 1. Download latest jQuery and jQuery Auto-complete plugin libraries.

Step 2. Included jQuery, auto-complete plugin js and its css files in the Page. This can be done in ASP.NET page, Master page, Control or WebPart:
    if (!Page.ClientScript.IsClientScriptBlockRegistered("jQueryScript"))
{
string path = "<script type='text/javascript' language='javascript'
src='/_layouts/Autocomplete/jQuery.js'></script>"
;
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "jQueryScript", path);
}
if (!Page.ClientScript.IsClientScriptBlockRegistered("AutocompleteScript"))
{
string path = "<script type='text/javascript' language='javascript'
src='/_layouts/Autocomplete/jquery.autocomplete.js'></script>"
;
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "AutocompleteScript", path);
}
if (!Page.ClientScript.IsClientScriptBlockRegistered("AutocompleteCss"))
{
string path = "<link rel='stylesheet' type='text/css'
href='/_layouts/Autocomplete/jquery.autocomplete.css' />"
;
Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "AutocompleteCss", path);
}

Step 3: Add following script on the page to enable the auto-complete on the textbox (WebUrl, ListName, FieldName need to be set in the code behind, or hard-coded in the script):
<script type="text/javascript">
$(document).ready(function () {
var handlerUrl = "/_layouts/Autocomplete/AutoCompleteHandler.ashx";
var request = handlerUrl + '?WebUrl=<%= WebUrl %>&ListName=<%= ListName %>&FieldName=<%= FieldName %>';
var options = { max: 20, multiple: true, multipleSeparator: ';' };
$("textarea[id='" + '<%= txtBox.ClientID %>' + "']").autocomplete(request, options);

});
</script>

Step 4: Add javascripts, css file, and the auto-complete HttpHandler inside the layout folder (C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\Autocomplete\). The auto-complete HttpHandler includes two parts:

AutoCompleteHandler.ashx:
<%@ Assembly Name="Project assembly fully qualified name" %>
<%@ WebHandler Language="C#" Class="AutoCompleteHandler" %>

AutoCompleteHandler.ashx.cs:
using System;
using System.Web;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

public class AutoCompleteHandler : IHttpHandler
{
public void ProcessRequest(HttpContext context)
{
string prefixText = string.Empty;
string inputText = context.Request["q"];
string webUrl = context.Request["WebUrl"];
string listName = context.Request["ListName"];
string fieldName = context.Request["FieldName"];
if (!string.IsNullOrEmpty(inputText))
{
int lastDelimiter = inputText.LastIndexOf(";");
lastDelimiter = (lastDelimiter > 0) ? lastDelimiter : 0;
prefixText = inputText.Substring(lastDelimiter).TrimStart(new char[] { ',', ';', ' ' });
}
if (string.IsNullOrEmpty(prefixText) || string.IsNullOrEmpty(webUrl)
|| string.IsNullOrEmpty(listName) || string.IsNullOrEmpty(fieldName))
{
return;
}

SendAutoComplete(context, inputText, webUrl, listName, fieldName);
}

private void SendAutoComplete(
HttpContext context,
string prefixText,
string webUrl,
string listName,
string fieldName)
{
List<string> nameList = new List<string>();
try
{
SPList list = SPListHeler.GetList(webUrl, listName);
if (list != null)
{
SPQuery spQuery = new SPQuery();
string query = @"<Where><BeginsWith>
<FieldRef Name='$NAME$'/>
<Value Type='Text'>$TEXT$</Value>
</BeginsWith></Where>"
;
spQuery.Query = query.Replace("$NAME$", fieldName).Replace("$TEXT$", prefixText);
int limit;
if (int.TryParse(context.Request["limit"], out limit) && limit > 0)
spQuery.RowLimit = (uint)limit;
SPListItemCollection items = list.GetItems(spQuery);
foreach (SPListItem item in items)
{
nameList.Add(item[fieldName].ToString());
}
}

nameList.Sort();
StringBuilder sb = new StringBuilder();
foreach (string tag in nameList)
{
sb.Append(tag + Environment.NewLine);
}

context.Response.Cache.SetCacheability(HttpCacheability.NoCache);
context.Response.ContentType = "text/plain";
context.Response.Write(sb.ToString() + " ");
}
catch (Exception ex)
{
// Log error
}
}

public bool IsReusable
{
get { return false; }
}
}

Note: To improve the performance, we can cache all the names and query the memory objects inside the http handler instead of CAML query each time.

Wednesday, October 07, 2009

SharePoint InputFormTextBox Validation

RequiredFieldValidator can not work with SharePoint InputFormTextBox. You have to client side JavaScript validation instead. There's an out-of-box function RTE_GetRichEditTextOnly come handy to retrieve the value from InputFormTextBox when doing JavaScript validation:
function ValidateInputFormTextbox() {
    var inputText = RTE_GetRichEditTextOnly("<%= inputControl.ClientID %>");
    if (inputText)
        return true;
    else
        return false;
}

Monday, September 28, 2009

Populate and Update SharePoint Choice Field

SPFieldChoice and SPFieldMultiChoice are two type of SharePoint Choice field. The first one is single selection and displayed as dropdown menu or the radio buttons; the latter is multiple selections and displayed as a list of check boxes. To populate the choice items in a custom webpart, we can simply go through the Choices collection in the choice field:
    // Populate Dropdown by SharePoint Choices (single selection from dropdown menu or radio buttons)
    private void PopulateSPChoiceDropDown(DropDownList dropdown, SPList list, string fieldName)
    {
        if (dropdown == null || list == null || list.ItemCount == 0)
            return;
            
        dropdown.Items.Clear();
        if (list != null && list.Fields.ContainsField(fieldName))
        {
            SPFieldChoice spChoices = list.Fields[fieldName] as SPFieldChoice;
            if (spChoices != null && spChoices.Choices != null)
            {
                for (int i = 0; i < spChoices.Choices.Count; i++)
                {
                    dropdown.Items.Add(new ListItem(spChoices.Choices[i], spChoices.Choices[i]));
                }
            }
        }
    }

    // Populate CheckBoxList by SharePoint Choices (muliple selction)
    private void PopulateSPChoiceCheckBoxList(CheckBoxList checkboxList, SPList list, string fieldName)
    {
        if (checkboxList == null || list == null || list.ItemCount == 0)
            return;
            
        checkboxList.Items.Clear();
        if (list != null && list.Fields.ContainsField(fieldName))
        {
            SPFieldMultiChoice spChoices = list.Fields[fieldName] as SPFieldMultiChoice;
            if (spChoices != null && spChoices.Choices != null)
            {
                for (int i = 0; i < spChoices.Choices.Count; i++)
                {
                    checkboxList.Items.Add(new ListItem(spChoices.Choices[i], spChoices.Choices[i]));
                }
            }
        }
    }
As a matter of fact, we can merge above two methods into one:
    // Populate List Control such as Dropdown and CheckBoxList by SharePoint Choices
    private void PopulateSPChoiceToListControl(ListControl listControl, SPList list, string fieldName)
    {
        if (listControl == null || list == null || list.ItemCount == 0)
            return;
      
        listControl.Items.Clear();
        if (list != null && list.Fields.ContainsField(fieldName))
        {
            SPFieldMultiChoice spChoices = list.Fields[fieldName] as SPFieldMultiChoice;
            if (spChoices != null && spChoices.Choices != null)
            {
                for (int i = 0; i < spChoices.Choices.Count; i++)
                {
                    listControl.Items.Add(new ListItem(spChoices.Choices[i], spChoices.Choices[i]));
                }
            }
        }
    }
The reason we can merge them together is because SPFieldChoice is inherited from SPFieldMultiChoice, and both DropDownList and CheckBoxList are implementation of abstract ListControl class. To update the SPField:
        SPFieldMultiChoiceValue values = new SPFieldMultiChoiceValue();
        foreach (ListItem listItem in checkList.Items)
        {
            if (listItem.Selected)
            {
                values.Add(listItem.Value);
            }
        }
        item["MultipleChoiceField"] = values;
        item["SingleChoiceField"] = dropdown.SelectedValue;
        item.Update();

Tuesday, September 15, 2009

SharePoint Batch Delete

It would be inefficient to delete many List items one by one when the List is huge. SharePoint provides a batch update mechanism using CAML. Following code snippet illustrates such usage where all records in a SharePoint List that have not been updated for more than 1 year will be deleted:
    void DeleteOldListItems(SPSite site, string webUrl, string listName)
    {
        StringBuilder sbDelete = new StringBuilder();
        sbDelete.Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?><Batch>");
        // CAML query for batch deleting
        string batchDeleteCaml = 
"<Method><SetList Scope=\"Request\">{0}</SetList><SetVar Name=\"ID\">{1}</SetVar><SetVar Name=\"Cmd\">Delete</SetVar></Method>"; // Query items not touched for more than a year; same as <Value Type="DateTime"><Today OffsetDays="-365" /></Value> string filterQueryCaml = @"<Where><Geq><FieldRef Name='Modified'/><Value Type='DateTime' IncludeTimeValue='TRUE'>" + SPUtility.CreateISO8601DateTimeFromSystemDateTime(DateTime.Now.AddYears(-1)) + "</Value></Geq></Where>"; using (SPWeb web = site.OpenWeb(webUrl)) { SPList list = web.Lists[listName]; SPQuery filterQuery = new SPQuery(); filterQuery.Query = filterQueryCaml; SPListItemCollection filterItems = list.GetItems(filterQuery); foreach (SPListItem item in filterItems) { sbDelete.Append(string.Format(batchDeleteCaml, list.ID.ToString(), item.ID.ToString())); } sbDelete.Append("</Batch>"); web.ProcessBatchData(sbDelete.ToString()); } }

Thursday, September 10, 2009

SharePoint SPList.ItemCount vs SPList.Items.Count

You can get item counts of a SharePoint list by SPList.ItemCount or SPList.Items.Count property. But these two numbers can be different. The inconsistency is due to the SharePoint content security. SPList.ItemCount shows the exact item number, but SPList.Items only returns the list items that current user has access to them.

For example, if a list has 10 items, and only 5 of 10 are accessible to you, then SPList.ItemCount = 10 and SPList.Items.Count = 5 in your security context.

In addition, SPList.ItemCount is more efficient than SPList.Items.Count, because SPList.ItemCount is just a property of the list, but SPList.Items.Count will result in loading all list items (with permissions) into memory.

Saturday, August 22, 2009

Get SPUsers From SharePoint PeoplePicker Control

Code below shows how to retrieve SPUsers from the SharePoint PeoplePicker control when PeoplePicker control is set to only allow users (SelectionSet="User"):
<SharePoint:PeopleEditor id="peoplePicker" runat="server" AllowEmpty="true" ValidatorEnabled="true" MultiSelect="true" SelectionSet="Group"/>
public static List<SPUser> GetPeopleEditorUsers(PeopleEditor picker)
{
List<SPUser> users = new List<SPUser>();
try
{
foreach (PickerEntity entity in picker.ResolvedEntities)
{
if ((string)entity.EntityData["PrincipalType"] == "User")
{
int userID = Int32.Parse(entity.EntityData["SPUserID"].ToString());
SPUser user = SPContext.Current.Web.SiteUsers.GetByID(userID);
if (user != null)
{
users.Add(user);
}
}
}
}
catch (Exception ex)
{
//Log error
}
return users;
}
The code needs to be revised a bit when groups are included in the PeoplePicker (SelectionSet="SPGroup,DL,SecGroup"):
public static List<SPUser> GetPeopleEditorGroupUsers(PeopleEditor picker)
{
List<SPUser> users = new List<SPUser>();
try
{
foreach (PickerEntity entity in picker.ResolvedEntities)
{
if ((string)entity.EntityData["PrincipalType"] == "SharePointGroup")
{
int groupID = int.Parse((string)entity.EntityData["SPGroupID"]);
SPGroup group = SPContext.Current.Web.SiteGroups.GetByID(groupID);
foreach (SPUser user in group.Users)
{
users.Add(user);
}
}
}
}
catch (Exception ex)
{
//Log error
}
return users;
}
The difference between SPWeb.Groups and SPWeb.SiteGroups:
  • SPWeb.Groups: Groups explicitly assigned persmissions in a site collection.
  • SPWeb.SiteGroups: All valid groups available in the site collection (superset of SPWeb.Groups). You can add a new group to SPWeb.SiteGroups, but not the SPWeb.Groups.

The difference between SPWeb.AllUsers, SPWeb.SiteUsers and SPWeb.Users:
  • SPWeb.Users: Users explicitly assigned permissions in a site collection.
  • SPWeb.SiteUsers: All valid users available in a site collection (superset of SPWeb.Users).
  • SPWeb.AllUsers: All users who are either members of the site or who have browsed to the site as authenticated members of a domain group in the site (superset of SPWeb.SiteUsers).

Monday, August 10, 2009

32 Character Limit Of SharePoint List's Internal Field Name

I came cross a weird problem and found this issue. A DateTime field with name of "KSNF WebPart Update Time" is used in a custom Feature, and I can get or set its value without any problem using SharePoint API:
DateTime updatetime = Convert.ToDateTime(item["KSNF WebPart Update Time"]);
But I got nothing when using CAML query. Yes, I use interal name of "KSNF_x0020_WebPart_x0020_Update_x0020_Time" in my CAML query, and it returns empty. I use SharePoint Manager to check this field and see that its internal static name is "KSNF_x0020_WebPart_x0020_Update_". It's trimmed to 32-character!

So SharePoint only allows 32-character in field's internal (static) name? I created a simply console application to test that:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;

public partial class Program
{
static void Main(string[] args)
{
string siteName = "http://localhost";
string listName = "Shared Documents";
string fieldName = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789012345";

Console.WriteLine("Adding SPField to a SharePoint Document Library and a List with name of '{0}'", fieldName);
Console.WriteLine();
Console.WriteLine("Get SPField info from updated SharePoint Document Library:");
TestFieldName(siteName, listName, fieldName);
Console.WriteLine();

listName = "Announcements";
Console.WriteLine("Get SPField info from updated SharePoint List:");
TestFieldName(siteName, listName, fieldName);

Console.Read();
}

private static void TestFieldName(string siteName, string listName, string fieldName)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists[listName];
if (!list.Fields.ContainsField(fieldName))
{
list.Fields.Add(fieldName, SPFieldType.Text, false);
list.Update();
}
if (list.Fields.ContainsField(fieldName))
{
SPField field = list.Fields[fieldName];
Console.WriteLine("Display name:\t'{0}' {1}Static name:\t'{2}'",
field.Title, System.Environment.NewLine, field.StaticName);

list.Fields.Delete(fieldName);
list.Update();
}
}
}
}
}
The result is:
Adding SPField to a SharePoint Document Library and a List with name of
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789012345'


Get SPField info from updated SharePoint Document Library:
Display name: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789012345'
Static name: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789012345'


Get SPField info from updated SharePoint List:
Display name: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789012345'
Static name: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef'
Apparently the 32-character limit for internal field name only applies to SharePoint lists. There's no such limit for SharePoint document libraries.

Wednesday, July 29, 2009

Uploading File To SharePoint Document Library

Uploading a file to SharePoint 2007 (WSS 3.0 or MOSS 2007) Document Library is very simple. Also Content Types and template files can be associated with a Document library. so we can create a new item using template file. Following code demos how to upload a file to SharePoint Document library from local file system and from the template file stored in SharePoint:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Microsoft.SharePoint;

class Program
{
static void Main(string[] args)
{
UploadFileToDocumentLibrary("http://localhost", "Shared Documents", @"c:\Test.doc");
InsertByTemplateFile("http://localhost", "Shared Documents", "Document");

Console.WriteLine("Done!");
Console.Read();
}

static void UploadFileToDocumentLibrary(string siteName, string listName, string fileName)
{
FileStream stream = File.OpenRead(fileName);
byte[] content = new byte[stream.Length];

stream.Read(content, 0, (int)stream.Length);
stream.Close();

using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb())
{
SPDocumentLibrary lib = web.Lists[listName] as SPDocumentLibrary;

//string docUrl = lib.RootFolder.Url + "/testupload.docx";
//SPFile newDoc = lib.RootFolder.Files.Add(docUrl, content, true);
SPFile newDoc = lib.RootFolder.Files.Add("testUpload.doc", content, true);

SPListItem item = newDoc.Item;
item["Title"] = "Test uploading file";
item.Update();

}
}
}

static void InsertByTemplateFile(string siteName, string listName, string contentTypeName)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb())
{
SPDocumentLibrary lib = web.Lists[listName] as SPDocumentLibrary;
SPContentType cType = lib.ContentTypes[contentTypeName];

SPFile cTypeTemplateFile = web.GetFile(cType.DocumentTemplateUrl);
byte[] content = cTypeTemplateFile.OpenBinary();

SPFile newDoc = lib.RootFolder.Files.Add("testTemplate.doc", content, true);

SPListItem item = newDoc.Item;
item["Title"] = "Test inserting by template file";
item["ContentTypeId"] = cType.Id;
item.Update();
}
}
}
}

Tuesday, July 14, 2009

Preserve System Fields When Copying SharePoint List Item

In previous post I explored the SharePoint List Item "Modified" field, which is considered as a system field. It will be exact the system time when the List item is created or updated. Sometimes we want to keep the original values of those fields when copying over List items, then we need to specifically assign those values. Following code snippet demos how to o preserve the original Created By, Created, Modified By and Modified fields:
        SPList origList = web.Lists.TryGetList("MyList");
        SPList newList = web.Lists.TryGetList("BackupList");
        foreach (SPListItem origItem in origList.Items)
        {
            SPListItem newItem = newList.Items.Add();
            newItem["Author"]   = origItem["Author"];   // Created By
            newItem["Created"]  = origItem["Created"];  // Created
            newItem["Editor"]   = origItem["Editor"];   // Modified By
            newItem["Modified"] = origItem["Modified"]; // Modified

            // Populate other fields

            newItem.Update();
        }

Friday, July 03, 2009

More On SharePoint Modified Field

I discussed the SharePoint "Modified" and "Last Modified" fields in my previous post. During the test I have an interesting finding on how update a list item will affect the "Modified" field. That's where this post is coming from.

In SharePoint there're two methods you can call to update a list item (SPListItem): SystemUpdate and Update. Following are their MSDN description(http://msdn.microsoft.com/en-us/library/ms438065.aspx):
  • SystemUpdate: Updates the database with changes that are made to the list item without changing the Modified or Modified By fields.
  • Update: Updates the database with changes that are made to the list item. (Overrides SPItem.Update().)
Obviously SystemUpdate method won't update the "Modified" field. What about the "Last Modified" field? Let's do a quick test:
public partial class Program
{
static void Main(string[] args)
{
string siteName = "http://localhost";
string listName = "Documents";
string itemInfo = string.Empty;

itemInfo = TestItemLastModifiedTime(siteName, listName, ExecutCommand.None);
Console.WriteLine("--Orignal--" + System.Environment.NewLine + itemInfo);
Console.WriteLine();

itemInfo = TestItemLastModifiedTime(siteName, listName, ExecutCommand.SystemUpdate);
Console.WriteLine("--After SystemUpdate--" + System.Environment.NewLine + itemInfo);
Console.WriteLine();
System.Threading.Thread.Sleep(2000);

itemInfo = TestItemLastModifiedTime(siteName, listName, ExecutCommand.Update);
Console.WriteLine("--After Update--" + System.Environment.NewLine + itemInfo);
Console.WriteLine();

Console.Read();
}

enum ExecutCommand
{
None,
SystemUpdate,
Update
}

private static string TestItemLastModifiedTime(string siteName, string listName, ExecutCommand command)
{
using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists[listName];
SPListItem item = list.Items[0];
switch (command)
{
case ExecutCommand.None:
return GetModifiedInfo(item);
case ExecutCommand.SystemUpdate:
item["Title"] = item.Title.Trim();
item.SystemUpdate(true);
break;
case ExecutCommand.Update:
item["Title"] = item.Title.Trim();
if (item.File.CheckOutStatus == SPFile.SPCheckOutStatus.None)
item.File.CheckOut();
item.Update();
break;
default:
return string.Empty;
}
return GetModifiedInfo(item);
}
}
}

private static string GetModifiedInfo(SPListItem item)
{
DateTime modified = Convert.ToDateTime(item["Modified"]);
DateTime lastModified = Convert.ToDateTime(item["Last_x0020_Modified"]);
string newline = System.Environment.NewLine;
string msg = string.Format("SPListItem Modified: \t\t{0}{1}SPListItem LastModified: \t{2}",
modified.ToString("yyyy-MM-dd HH:mm:ss"), newline, lastModified.ToString("yyyy-MM-dd HH:mm:ss"));
return msg;
}
}
The result:
--Orignal--
SPListItem Modified: 2009-07-02 11:33:20
SPListItem LastModified: 2009-07-02 11:33:20

--After SystemUpdate--
SPListItem Modified: 2009-07-02 11:33:20
SPListItem LastModified: 2009-07-02 11:33:20

--After Update--
SPListItem Modified: 2009-07-03 21:15:16
SPListItem LastModified: 2009-07-02 11:33:20
It clearly shows that both SystemUpdate and Update Methods don't have any impact on "Last Modified" field.

All SharePoint data, including all version data, are stored in AllUserData table in the SharePoint backend content database. It looks like that both "Modified" and "Last Modified" time are from "tp_Modified" column in AllUserData table: "Modified" from the latest version while "Last Modified" from the last major version. You can verify this by checking the content database:
SELECT tp_Modified, tp_UIVersionString FROM AllUserData
WHERE tp_DirName = 'Documents' AND tp_LeafName = 'MyDocument1'
ORDER BY tp_Modified DESC

--return:
tp_Modified tp_UIVersionString
2009-07-02 11:33:20.000 5.1
2009-07-03 21:15:16.000 5.0
2009-06-15 17:33:59.000 4.1

Wednesday, July 01, 2009

SharePoint Modified Field And Last Modified Field

We know there's a "Modified" field in SharePoint that records the last modified time of a SPListItem. This field is viewable and you can add it to any SharePoint views. You will see the time of "Modified" field is change when you do an update on the list item.

There's another hidden "Modified" field, also known as "Last Modified" field with internal name of "Last_x0020_Modified". What's difference? Following are their official description from MSDN:
  • Modified: Identifies a field that contains the last modified date and time information that is associated with the specified SharePoint Foundation object.
  • Last_x0020_Modified: Identifies a field that contains version control information for the last modified version of the specified SharePoint Foundation list object.
The description looks not that clear. Which one should we use in our work? Let's examine them in details. First look at their field definition in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\FEATURES\fields\fieldswss.xml:
   <Field ID="{28cf69c5-fa48-462a-b5cd-27b6f9d2bd5f}"
Name="Modified"
SourceID="http://schemas.microsoft.com/sharepoint/v3"
StaticName="Modified"
Group="_Hidden"
ColName="tp_Modified"
RowOrdinal="0"
ReadOnly="TRUE"
Type="DateTime"
DisplayName="$Resources:core,Modified;"
StorageTZ="TRUE">
</Field>
<Field ID="{173f76c8-aebd-446a-9bc9-769a2bd2c18f}"
Name="Last_x0020_Modified"
SourceID="http://schemas.microsoft.com/sharepoint/v3"
StaticName="Last_x0020_Modified"
Group="_Hidden"
ReadOnly="TRUE"
Hidden="TRUE"
DisplayName="$Resources:core,Modified;"
Type="Lookup"
List="Docs"
FieldRef="ID"
ShowField="TimeLastModified"
Format="TRUE"
JoinColName="DoclibRowId"
JoinRowOrdinal="0"
JoinType="INNER">
</Field>
Using SharePoint Manager we can see the raw "Modified" and "Last Modified" value (XML) stored in a list item:
    <?xml version="1.0" encoding="utf-16"?>
<z:row xmlns:z="#RowsetSchema"
ows_Modified="2009-06-15 11:23:13"
ows_Last_x0020_Modified="23;#2009-06-15 11:23:13"
... />
As we can see "Modified" field is of DateTime type and real DateTime is stored in SharePoint content database; on the other hand, "Last_x0020_Modified" field is a lookup column pointing to "Docs" list's "TimeLastModified" column.

With further investigation, I found that this "Docs" list and the "TimeLastModified" field do not really exist. They are virtual and the values are determinted at run time. I tried to figure out what SharePoint exactully is doing to obtain this lookup value. So I did a quick test on these two fields:
    public partial class Program
{
static void Main(string[] args)
{
string siteName = "http://localhost";
string listName = "Documents";
using (SPSite site = new SPSite(siteName))
{
using (SPWeb web = site.OpenWeb())
{
SPList list = web.Lists[listName];
DateTime modifiedTime, lastModifiedTime;

SPQuery query = new SPQuery();
query.ViewAttributes = "Scope='Recursive'";
query.ViewFields = "<FieldRef Name='Modified' Nullable='TRUE'/>"
+"<FieldRef Name='Last_x0020_Modified' Nullable='TRUE'/>";

SPListItemCollection items = list.GetItems(query);
modifiedTime = Convert.ToDateTime(items[0]["Modified"]);
lastModifiedTime = Convert.ToDateTime(items[0]["Last_x0020_Modified"]);
Console.WriteLine("Modified: " + lastModifiedTime.ToString()
+ " Last Modified: " + lastModifiedTime.ToString());
}
}
Console.Read();
}
}
The code is simple. I use CAML query to get the "Modified" and "Last Modified" data without any filter. I saw two quries sent to SharePoint content database for such CAML query. The first one is:

exec proc_GetListFields '2E1D8267-FBA0-4995-8CBB-08E747FB54D7','750DB8DD-A6AA-49B5-9D23-EB6E4B95EAD7'
Notice two GUIDs passing in this query do not match the ID of "Modified" or "Last Modified". The query returns:
    <FieldRef Name="ContentTypeId" />
<FieldRef Name="Title" ColName="nvarchar1" />
<FieldRef Name="_ModerationComments" ColName="ntext1" />
<FieldRef Name="File_x0020_Type" ColName="nvarchar2" />
<FieldRef Name="Name" ColName="nvarchar3" />
<FieldRef Name="EMail" ColName="nvarchar4" />
<FieldRef Name="Notes" ColName="ntext2" />
<FieldRef Name="SipAddress" ColName="nvarchar5" />
<FieldRef Name="Locale" ColName="int1" />
<FieldRef Name="CalendarType" ColName="int2" />
<FieldRef Name="AdjustHijriDays" ColName="int3" />
<FieldRef Name="TimeZone" ColName="int4" />
<FieldRef Name="Time24" ColName="bit1" />
<FieldRef Name="AltCalendarType" ColName="int5" />
<FieldRef Name="CalendarViewOptions" ColName="int6" />
<FieldRef Name="WorkDays" ColName="int7" />
<FieldRef Name="WorkDayStartHour" ColName="int8" />
<FieldRef Name="WorkDayEndHour" ColName="int9" />
<FieldRef Name="IsSiteAdmin" ColName="bit2" />
<FieldRef Name="Deleted" ColName="bit3" />
<FieldRef Name="Picture" ColName="nvarchar6" ColName2="nvarchar7" />
<FieldRef Name="Department" ColName="nvarchar8" />
<FieldRef Name="JobTitle" ColName="nvarchar9" />
<FieldRef Name="IsActive" ColName="bit4" />

The second query sent to database is massive:

exec sp_executesql N' SELECT TOP 2147483648 t2.[tp_Created] AS c3c8,t1.[Type] AS c0,t1.[TimeLastModified] AS c9,t3.[tp_ID] AS c10c5,CASE WHEN DATALENGTH(t1.DirName) = 0 THEN t1.LeafName WHEN DATALENGTH(t1.LeafName) = 0 THEN t1.DirName ELSE t1.DirName + N''/'' + t1.LeafName END AS c11,t1.[ScopeId] AS c16,UserData.[nvarchar4],UserData.[tp_CheckoutUserId],UserData.[tp_Version],t1.[Id] AS c15,t2.[nvarchar5] AS c3c7,t3.[nvarchar1] AS c10c4,UserData.[tp_HasCopyDestinations],UserData.[tp_ModerationStatus],UserData.[tp_Level],t2.[nvarchar1] AS c3c4,t2.[nvarchar4] AS c3c6,t3.[nvarchar4] AS c10c6,t3.[tp_Created] AS c10c8,t1.[MetaInfo] AS c14,t1.[LeafName] AS c2,UserData.[tp_Modified],UserData.[nvarchar3],t2.[tp_ID] AS c3c5,t3.[nvarchar5] AS c10c7,UserData.[tp_ID],t1.[ProgId] AS
c13,UserData.[tp_CopySource],t1.[TimeCreated] AS c1,UserData.[tp_Editor],t1.[IsCheckoutToLocal] AS c12 FROM UserData INNER MERGE JOIN Docs AS t1 WITH(NOLOCK) ON ( 1 = 1 AND UserData.[tp_RowOrdinal] = 0 AND t1.SiteId = UserData.tp_SiteId AND t1.SiteId = @L2 AND t1.DirName = UserData.tp_DirName AND t1.LeafName = UserData.tp_LeafName AND t1.Level = UserData.tp_Level AND (UserData.tp_Level = 255 AND t1.LTCheckoutUserId =@IU OR (UserData.tp_Level = 1 AND (UserData.tp_DraftOwnerId IS NULL OR (UserData.tp_DraftOwnerId <>@IU AND 1=0 )) OR UserData.tp_Level = 2 AND (UserData.tp_DraftOwnerId = @IU OR 1=1 )) AND (t1.LTCheckoutUserId IS NULL OR t1.LTCheckoutUserId <> @IU )) AND (1 = 1)) LEFT OUTER JOIN AllUserData AS t2 WITH(NOLOCK, INDEX=AllUserData_PK) ON (UserData.[tp_Editor]=t2.[tp_ID] AND UserData.[tp_RowOrdinal] = 0 AND t2.[tp_RowOrdinal] = 0 AND ( (t2.tp_IsCurrent = 1) ) AND t2.[tp_CalculatedVersion] = 0 AND t2.[tp_DeleteTransactionId] = 0x AND t2.tp_ListId = @L3 AND UserData.tp_ListId = @L4) LEFT OUTER JOIN AllUserData AS t3 WITH(NOLOCK, INDEX=AllUserData_PK) ON (UserData.[tp_CheckoutUserId]=t3.[tp_ID] AND UserData.[tp_RowOrdinal] = 0 AND t3.[tp_RowOrdinal] = 0 AND ( (t3.tp_IsCurrent = 1) ) AND t3.[tp_CalculatedVersion] = 0 AND t3.[tp_DeleteTransactionId] = 0x AND t3.tp_ListId = @L3 AND UserData.tp_ListId = @L4) WHERE (UserData.tp_Level= 255 AND UserData.tp_CheckoutUserId = @IU OR ( UserData.tp_Level = 2 AND UserData.tp_DraftOwnerId IS NOT NULL OR UserData.tp_Level = 1 AND UserData.tp_DraftOwnerId IS NULL ) AND ( UserData.tp_CheckoutUserId IS NULL OR UserData.tp_CheckoutUserId <> @IU)) AND UserData.tp_SiteId=@L2 AND (UserData.tp_DirName=@DN) AND UserData.tp_RowOrdinal=0 AND (t1.SiteId=@L2 AND (t1.DirName=@DN)) ORDER BY t1.[Type] Desc,UserData.[tp_ID] Asc OPTION (FORCE ORDER) ',N'@L0 uniqueidentifier,@L2 uniqueidentifier,@IU int,@L3 uniqueidentifier,@L4 uniqueidentifier,@DN nvarchar(260)',@L0='00000000-0000-0000-0000-000000000000',@L2='3640A558-D026-4ABF-BB7F-549ECA727EEC',@IU=3,@L3='750DB8DD-A6AA-49B5-9D23-EB6E4B95EAD7',@L4='EE1ADF17-47DB-4065-A8DC-8638E6903484',@DN=N'Documents'

I have to admit all that is just too magical for me to understand what's under the hood. So go with reflector, but I found it's even more complex than such a database query when I traced the code behind a CAML query from the SharePoint dll. Okay I gave up my path here. What I can do is to verify when the value of these two fields are different.

What I found is that all change on a list item conducted through UI would affect its "Modified" field immediately. Updating a list item by code is a different story. I will cover that in next post.

It looks like the "Last Modified" field is used internally by SharePoint itself. We should use "Modified" field in our logic to trace a list item's last modified time in general.

Monday, June 22, 2009

Windows Live Integration With ASP.NET

Windows Live ID Introduction

Similar to Facebook Connect (See previous article), Windows Live ID is also an authentication model with broader services available. It is a service that assigns cross-site identifiers to users throughout the Internet. From Microsoft’s definition: "The Windows Live ID service (formerly known as the Passport Network) is the identity and authentication system provided by Windows Live. More than 380 million users have credentials that work with Windows Live ID."

There are many Live services available from Microsoft using Windows Live ID. Microsoft have paid a lot of effort to consolidate all Live services in one platform called Live Framework (LiveFX), and offers an uniform way for programming Live Services from a variety of platforms, programming languages, applications and devices. For a simple contact sharing, there is no need to jump into LiveFX.

Three main Live SDKs are available for developers to integrate third-party applications with Live services:

The Windows Live ID Client 1.0 SDK makes it easy to start developing identity-aware client (desktop) applications.
The Windows Live ID Delegated Authentication SDK for Application Providers helps to get permission to access users' information on Windows Live services.
The Windows Live ID Web Authentication SDK provides a platform-independent interface and helps to authenticate users. This only offers distributed authentication without data sharing.

We will use the Windows Live ID Delegated Authentication SDK to access user’s contact list as an example.

There is an important consent concept for Live data access. The application requests data from Live service, Live service requires user to sign in, and will prompt user to grant the consent of data sharing. If user agrees to that, an encrypted consent token will be sent to the consent handler (channel file) in the application by query string, consent handler is able to decrypt the consent token because it knows the secret code generated during Live service registration (see next section for details). With a valid consent token, the application will have permission to access the Live service data. The detail of how it works is well described in http://msdn.microsoft.com/en-us/library/cc287613.aspx. Following figure illustrates the information flow of delegated authentication with consent token.


Note that all messages are passing through URL or client Cookies in above figure.

Register Windows Live Service

Like Facebook Connect, in order to user Windows Live Service, we need to register our application with Windows Live services which now is part of Azure services.

Go to http://lx.azure.microsoft.com, login with valid Windows Live ID, and there shows the Azure Services Developer Portal:


Click New project button and select Live Service Existing APIs. Then in the project creation page, input whatever project name and description, but make sure the Domain and Return URL are matching the application settings because they are used for cross domain call-back communication. Here we input windowslivetest.com as domain and http://windowslivetest.com/ as Return URL. Once click the Create button a new Windows Live application will be registered.

Remember Applicaiton ID and Secret key entries on the summary page are very important because we need to use them in our application to use the Windows Live service. We need to write them down and will put them in our web application configuration file web.config later.

At this point the Windows Live service registration for an application is completed. In next section we will build a simple web application to retrieve data from Windows Live service we just registered.

Getting Windows Live Data

After the registration on Windows Live service, we can build an application to consume Windows Live data. Here we create a web application to retrieve user's contact list and show the name and email for each contact on the page. For more details to use Windows Live server, refer to SDK samples and the Windows Live Data Interactive SDK (https://dev.live.com/livedata/sdk/Default.aspx).

Before creating the page, we need to copy the WindowsLiveLogin.cs file from SDK to our solution, which includes a helper class named WindowsLiveLogin.

We also need to create a few application settings in web.config file:
  <appSettings>
<!-- Windows Live Settings -->
<add key="wll_appid" value="000000004001710E"/>
<add key="wll_secret" value="yQvEotqjpxJPHW9jDAtc9ckepX2lZirK"/>
<add key="wll_securityalgorithm" value="wsignin1.0"/>
<add key="wll_returnurl" value="http://windowslivetest.com/WindowsLiveHandler.aspx" />
<add key="wll_policyurl" value="http://windowslivetest.com/LivePrivacy.htm" />
</appSettings>
Application ID and Secret come from the Windows Live service registration in previous section. The privacy page (LivePrivacy.htm) can be empty, and the channel file WindowsLiveHandler.aspx is for cross-domain communication. It is responsible for setting the consent token to cookie and redirecting to main page (no front end html content):
namespace Web
{
public partial class WindowsLiveHandler : System.Web.UI.Page
{
// Initialize the WindowsLiveLogin module.
static WindowsLiveLogin WLLogin = new WindowsLiveLogin(true);

protected void Page_Load(object sender, EventArgs e)
{
HttpRequest request = HttpContext.Current.Request;
HttpResponse response = HttpContext.Current.Response;

string appctx = "~/WindowsLiveTest.aspx";

string action = request["action"];

if (action == "delauth")
{
// Get the consent token
WindowsLiveLogin.ConsentToken ct = WLLogin.ProcessConsent(request.Form);
HttpCookie authCookie = new HttpCookie("delauthtoken");
if (ct != null)
{
authCookie.Value = ct.Token;
authCookie.Expires = DateTime.Now.AddYears(10);
}
else
{
authCookie.Expires = DateTime.Now.AddYears(-10);
}

response.Cookies.Add(authCookie);
response.Redirect(appctx);
response.End();
}
else
{
response.Redirect(appctx);
response.End();
}
}
}
}
The main page (WindowsLiveTest.aspx) checks the consent token from cookie. If the token is valid, it will connect to the Windows Live service with the consent token, parse the XML return using Linq to XML, and show the contacts’ name and email in a GridView:
namespace Web
{
public partial class _WindowsLive : System.Web.UI.Page
{
// Initialize the WindowsLiveLogin module.
static WindowsLiveLogin wll = new WindowsLiveLogin(true);
protected WindowsLiveLogin.ConsentToken ConsentToken = null;

/// <summary>
/// Handles the page load event
/// </summary>
protected void Page_Load(object sender, EventArgs e)
{
HttpCookie authCookie = HttpContext.Current.Request.Cookies["delauthtoken"];

// If the raw consent token has been cached in a site cookie,
// process it and extract the consent token.
if (authCookie != null)
{
string t = authCookie.Value;
ConsentToken = wll.ProcessConsentToken(t);

if ((ConsentToken != null) && !ConsentToken.IsValid())
{
ConsentToken = null;
return;
}

string uri = "https://livecontacts.services.live.com/users/@L@" +
ConsentToken.LocationID + "/rest/LiveContacts";

HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
request.ContentType = "application/xml; charset=utf-8";
request.Method = "GET";

// Add the delegation token to a request header.
request.Headers.Add("Authorization", "DelegatedToken dt=\"" + ConsentToken.DelegationToken + "\"");

//Issue the HTTP GET request to Windows Live Contacts.
HttpWebResponse response = (HttpWebResponse)request.GetResponse();

// Create XDocument by returned XML
StreamReader readStream = new StreamReader(
response.GetResponseStream(), System.Text.Encoding.UTF8);
XmlReader xmlReader = XmlReader.Create(readStream);
XDocument xDoc = XDocument.Load(xmlReader);
response.Close();

// Summary format:
//Signed in user {username}({email}) has {count} contacts
if (xDoc.Descendants("Owner").Elements("WindowsLiveID").Any())
{
string userInfo = string.Format(
"<p>Signed in user {0}({1}) has {2} contacts</p>",
xDoc.Descendants("Owner").Elements("Profiles").Elements("Personal").Elements("DisplayName").Any() ?
xDoc.Descendants("Owner").Elements("Profiles").Elements("Personal").Elements("DisplayName").First().Value : "",
xDoc.Descendants("Owner").Elements("WindowsLiveID").First(),
xDoc.Descendants("Contact").Count());

lblUserInfo.Text = userInfo;
}

// Query contact display name and email address
var contacts = from contact in xDoc.Descendants("Contact")
select new
{
Name = contact.Elements("Profiles").Elements("Personal").Elements("DisplayName").Any() ?
contact.Element("Profiles").Element("Personal").Element("DisplayName").Value : string.Empty,
Email = contact.Elements("Emails").Elements("Email").Elements("Address").Any() ?
contact.Element("Emails").Element("Email").Element("Address").Value : string.Empty
};

// Bind list of display name and email address to GridView
GridView1.DataSource = contacts;
GridView1.DataBind();
divConsent.Visible = true;
}
else
{
string appSite = "https://" + Request.Url.Host;
string delegationURL = wll.GetConsentUrl("ContactsSync.FullSync");
string grantLink = string.Format("<a href='" + delegationURL +
"'>Click to grant this application access to your Windows Live data</a>", appSite);
Page.Response.Write(grantLink);
}
}
}
}
The page's html markup:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WindowsLiveTest.aspx.cs" Inherits="Web._WindowsLive" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>Windows Live Test</title>
<script src="Scripts/jquery-1.3.2.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#tblConsentDetail").hide()
});
function toggleConsentDetail() {
$(
"#tblConsentDetail").toggle();
}
</script>
</head>
<body>
<form id="form1" runat="server">
<div id="divConsent" runat="server" visible="false">
<a href="https://consent.live.com/ManageConsent.aspx">Manage your consent</a>&nbsp;&nbsp;
<a href="#" onclick="javascript:toggleConsentDetail();">Consent Token Details</a>&nbsp;&nbsp;
<a href="WindowsLiveHandler.aspx?action=delauth" >Delete the token from this session</a><br />
<asp:Label ID="lblUserInfo" runat="server"></asp:Label>
<table id="tblConsentDetail" cellpadding="5">
<tr valign="top"><td style="width:300px;">LID</td><td>
<%=ConsentToken.LocationID%></td></tr>
<tr valign="top"><td>Delegation token</td><td>
<%=ConsentToken.DelegationToken%></td></tr>
<tr valign="top"><td>Refresh token</td><td>
<%=ConsentToken.RefreshToken%></td></tr>
<tr valign="top"><td>Expiry</td><td>
<%=ConsentToken.Expiry%></td></tr>
<tr valign="top"><td>Offers</td><td>
<%=ConsentToken.OffersString%></td></tr>
<tr valign="top"><td>Context</td><td>
<%=ConsentToken.Context%></td></tr>
<tr valign="top"><td>Token</td><td>
<%=ConsentToken.Token%></td></tr>
</table>
</div>
<br />
<asp:GridView ID="GridView1" runat="server" GridLines="None" CellPadding="5" ForeColor="#333333" >
<RowStyle BackColor="#EFF3FB" />
<FooterStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
<PagerStyle BackColor="#2461BF" ForeColor="White" HorizontalAlign="Center" />
<EmptyDataTemplate>
<asp:Label ID="Label1" runat="server">There are no contacts</asp:Label>
</EmptyDataTemplate>
<SelectedRowStyle BackColor="#D1DDF1" Font-Bold="True" ForeColor="#333333" />
<HeaderStyle BackColor="#507CD1" Font-Bold="True" ForeColor="White" />
<EditRowStyle BackColor="#2461BF" />
<AlternatingRowStyle BackColor="White" />
</asp:GridView>
</form>
</body>
</html>
There is no consent token available when we first visit the page, and the grant access like is displayed:

Click the link page will be redirected to the Windows Live login page:

After input valid Windows Live ID and password, an agreement page pops up to let user to confirm the data sharing:

When user clicks the Allow access button, page will direct to our test page (underneath the channel page is called back first). Since the consent token now is available, the test page will show user’s contact:

When the delegated authentication doesn’t work properly, first thing to diagnose the issue is check the error code inside the URL, and look up the details of the error from Live SDK error list page: http://msdn.microsoft.com/en-us/library/cc287686.aspx.

Using Windows Live Contacts Control

In addition to Windows Live service SDK(API), the Windows Live Contacts Control (Beta) (http://dev.live.com/contactscontrol/) abstracts the related functions into a HTML control and makes the whole Windows Live contact sharing much easier to use.

Windows Live Contacts Control does not require registering with Windows Live service, which significantly simplifies the contact import. Just reference two JavaScript files from Windows Live, and declare the Contacts control on an html or aspx page as following:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:devlive="http://dev.live.com">
<head>
<title>Windows Live Contacts Control Test</title>
<script type="text/javascript" src="https://controls.services.live.com/scripts/base/v0.3/live.js"></script>
<script type="text/javascript" src="https://controls.services.live.com/scripts/base/v0.3/controls.js"></script>
<script type="text/javascript">
function receiveData(contacts) {
var s = "<p>" + contacts.length + " contacts are imported:</p>";
for (var i = 0; i < contacts.length; i++) {
s += "<p>Contact " + (i+1) + "<br>";
for (var j in contacts[i]) {
s += escapeString(j) +
": " + escapeString(contacts[i][j]) + "<br>";
}
s +=
"</p>";
}
document.getElementById('divStatus').innerHTML = s;
}
function signin() {
document.getElementById('divStatus').innerHTML =
"You're logged in!";
};

function signout() {
document.getElementById('divStatus').innerHTML =
"You need to log in.";
};
function showerror(msg) {
document.getElementById('divStatus').innerHTML = escapeString(msg);
}
function escapeString(str) {
return str.replace(/&/g,
"&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").
replace(/\"/g,
"&quot;").replace(/\'/g, "&#39;");
}
function controlLoad() {
document.getElementById('divStatus').innerHTML =
"You need to log in.";
}
</script>
</head>
<body>
<P>Windows Live Contacts Control Test</P>
<devlive:contactscontrol
style="width:280px;height:400px;float:left;margin-right:10px;"
id="ContactsControl"
devlive:privacyStatementURL="LivePrivacy.htm"
devlive:dataDesired="name,firstname,lastname,email,emailpersonal,emailbusiness"
devlive:onData="receiveData"
devlive:onSignin="{signin();}"
devlive:onSignout="{signout();}"
devlive:onError="showerror"
devlive:channelEndpointURL="LiveChannel.htm"
devlive:"controlLoad">
</devlive:contactscontrol>
<div id="divStatus"></div>
</body>
</html>
The property and event description of Windows Live Contacts Control is available in http://dev.live.com/contactscontrol/api.aspx. The channel page for cross domain communication is also required. In above case, we create a LiveChannel.htm page:
 <html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Channel</title>
<meta name="ROBOTS" content="NONE" />
<script type="text/javascript">
function doc_loaded() {
try {
var hash = window.location.hash.substr(1);
if (window.location.replace == null)
window.location.replace = window.location.assign;
window.location.replace("about:blank");
var name = hash.split("/")[0];
var win = null;
if (name)
win = window.parent.frames[name];
else
win = window.parent.parent;
win.Microsoft.Live.Channels.Mux._recv_chunk(hash);
}
catch (ex) {
/* ignore */
}
}
</script>
</head>
<body onload="doc_loaded()">
</body>
</html>
An empty window similar to Windows Messenger is displayed when the page is first loaded. You click on Signin, and when the login is successful, the popup window will close automatically and the user’s contacts will show on the control:

You can select the contacts needed to be shared, and click the Share contacts button inside the control, and then an agreement page will pop up asking the user to confirm the data sharing:

The popup page will close when user clicks Send button, and the contacts are imported to the page hosting the control:


The Windows Live Contacts Control is handy to import email addresses. But be aware that the control is still in Beta phase, and its interface could be changed later.

Known Issues

The local time stamp of browser machine is sent in the request URL for a delegated Live authentication service. Syncing the time of development machine is important for testing. Nowadays many developers are using virtual machine as a development platform, as virtual machines are flexible, easy to maintain and test. But be cautious that virtual machines’ time may not be correct if they are manually turned off and on. The Live services will fail if time variance between servers for Live services and the client machine is too much.

Windows Live ID is upgraded from previous Passport services. It’s still in early 1.0 version, and some of the Live APIs are actually in Beta phase. The bigger picture of Microsoft Azure services have just jumped to the stage in less than a year. I did encounter the unstable services during the tests.

Windows Live Web Authentication API currently is totally separated from Delegated Authentication API. Logging out of Windows Live is a basic function defined in We Authentication but not in Delegated Authentication service. The page will be redirected to http://www.msn.com instead of original application page, if we use the same Web Authentication logout URL in Delegated Authentication.
The workaround to the logout problem can be making a cross domain AJAX call (such as getJSON in jQuery) to the logout URL, or create an invisible iframe for the logout call:
    <script type="text/javascript">
function logoutLive() {
var $frame = $("<iframe width='1px' height='1px'
src='http://login.live.com/logout.srf?appid=<%=LiveAPPID %>'>"
);
$('#divContent').append($frame);
$.cookie('delauthtoken', null);
location.reload();
}
</script>
<body>
<form id="form1" runat="server">
<div id="divContent">
<a href="javascript:logoutLive();">Logout Live</a>
</div>
</form>
</body>

Conclusion

Windows Live integeration works but it seems not as easy/stable as Facebook Live.