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.