Tuesday, March 30, 2010

Telerik Editor Issue When Overriding Page Render Method

Telerik RAD Editor is used in one SharePoint WCM site. To include Flash objects and scripts inside RAD Editor, I did following steps:
1. Add "<tool name='FlashManager' />" to Editor configuration file in C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\wpresources\RadEditorSharePoint\5.5.1.0__1f131a624888eeed\ToolsFile.xml. This ensures the flash manager icon is available in edit window. Options for adding this setting:
2. Update all RADEditor field controls in page layout from:
<telerik:RadHtmlField id="RadHtmlField1" FieldName="fieldName" runat="server" CssClass="telerikeditor" ></telerik:RadHtmlField>

To:

<telerik:RadHtmlField id="RadHtmlField1" FieldName="fieldName" runat="server" CssClass="telerikeditor" AllowScripts="true" AllowSpecialTags="true"></telerik:RadHtmlField>

This makes scripts and special tags (such as flash <object> tag) legal inside RADEditor.

But the scripts and flash content were not showing. The source html shows something like:
<pre style="display:none" id=RadEditorEncodedTag>....</pre>

I googled and couldn't see a fix about this issue. So I used reflector to find out how Telerik does under the hood. And I figured out that RAD Editor stores non-standard tags like flash objects and scripts in following format:
<pre style="display: none;" id="RadEditorEncodedTag">Base64codingObjects</pre>

RADEditor field Control will decode it during render cycle:
protected override void RenderFieldForDisplay(HtmlTextWriter output)
{
if (!string.IsNullOrEmpty((string) this.ItemFieldValue))
{
bool canCacheResults = true;
string html = HtmlEditorInternal.ConvertStorageFormatToViewFormat(
(string) this.ItemFieldValue, out canCacheResults);
output.Write(EditorTools.UnEscapeSpecialTags(html));
}
}
The problem is that Render Method is overridden in the custom page layouts, and this RenderFieldForDisplay is not invoked. I then added such decoding logic to the overridden Render method:
        protected override void Render(HtmlTextWriter writer)
{
//...
StringBuilder sb = new StringBuilder();
StringWriter sw = new StringWriter(sb);
HtmlTextWriter ourWriter = new HtmlTextWriter(sw);
base.Render(ourWriter);
string writeString = EscapeRADEditorTags(sb.ToString());
Response.Write(writeString);
//...
}

private static string EscapeRADEditorTags(string html)
{
try
{
MatchEvaluator evaluator = new MatchEvaluator(DecodeMatchManually);
return Regex.Replace(html,
"<pre\\s+(style=\"display\\s*:none\"\\s*)?id=\"?RadEditorEncodedTag\"?[^>]*>([^<]+)</pre>",
evaluator, RegexOptions.ECMAScript | RegexOptions.Multiline | RegexOptions.IgnoreCase);
}
catch (Exception ex)
{
Logger.LogException(ex);
return html;
}
}
Everything works properly after this code change.

Monday, March 22, 2010

HttpModule Blocks SharePoint Designer Issue

A HttpModule is added to a SharePoint publishing site. Every thing works fine except that SharePoint designer is not able to open the SharePoint site. After tracing the html traffic, I noticed some communication patterns between SharePoint designer and the SharePoint site. So the workaround is to skip all HttpModule actions for those communications. Following code is added inside the HttpModule:
    if ( httpContext.Request.UserAgent.Contains("WebDAV") 
|| httpContext.Request.UserAgent.Contains("MS FrontPage")
|| httpContext.Request.Url.AbsoluteUri.Contains("_vti_"))
{
return;
}
Now SharePoint designer can open the site without any problem. But sometimes the check-out function inside SharePoint designer is disable for some reason. I can not figure out why that happens sometimes but not always.

Tuesday, January 26, 2010

SharePoint Limit Of 11 Direct Dependencies

MOSS 2007 Error

"An error occurred during the process of . The page '/_catalogs/masterpage/Support.master' allows a limit of 11 direct dependencies, and that limit has been exceeded.":




Resolution

Search following default setting in web.config:
    <SafeMode MaxControls="200" CallStack="false" DirectFileDependencies="10" 
TotalFileDependencies="50" AllowPageLevelTrace="false">
<PageParserPaths>
</PageParserPaths>
</SafeMode>
Change DirectFileDependencies value from 10 to a bigger number.

Monday, January 11, 2010

Custom Properties In SharePoint Custom Field Type

There are quite a lot of discussions in Internet regarding how to create custom field type in SharePoint with custom properties. Some are not working and some are way more too complicated.

I found the best and the easiest way to do this is use WSPBuilder which can be downloaded at http://wspbuilder.codeplex.com. It's a free tool for SharePoint and it simplifies the SharePoint development.

Once WSPBuilder is installed, Visual Studio 2005/2008 will include WSPBuilder extension menu context. To create a custom field type, first you create a WSPBuilder project, and right click the project to add a new item of "Custom Field Type"; then all those the field control, field editor control, field type definition xml, and field type classes, are all well created in certain folder. Under the FieldTypeCode folder, there's a CustomFieldType.cs class where the custom field type is defined. This field type class also includes a custom property. In order to add a new custom property, simply add a regular .NET property and add the its names to a static string array named "CustomPropertyNames", then all done! It works like a charm.

Carsten Keutmann is the creator of WSPBuilder. He seems to have very deep understanding of SharePoint. Another very popular free SharePoint utility, SharePoint Manager 2007, is also written by him. Microsoft really should consider to hire such genius people from the community.

The default CustomeFiledTyp.cs generated by WSPBuilder:
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using System.Web.UI;
using System.Web.UI.WebControls;


namespace WSPBuilderProject1
{
public class CustomFieldType1 : SPFieldText
{
private static string[] CustomPropertyNames = new string[] { "MyCustomProperty" };

public CustomFieldType1(SPFieldCollection fields, string fieldName)
: base(fields, fieldName)
{
InitProperties();
}

public CustomFieldType1(SPFieldCollection fields, string typeName, string displayName)
: base(fields, typeName, displayName)
{
InitProperties();
}

#region Property storage and bug workarounds - do not edit

/// <summary>
/// Indicates that the field is being created rather than edited. This is necessary to
/// work around some bugs in field creation.
/// </summary>
public bool IsNew
{
get { return _IsNew; }
set { _IsNew = value; }
}
private bool _IsNew = false;

/// <summary>
/// Backing fields for custom properties. Using a dictionary to make it easier to abstract
/// details of working around SharePoint bugs.
/// </summary>
private Dictionary<string, string> CustomProperties = new Dictionary<string, string>();

/// <summary>
/// Static store to transfer custom properties between instances. This is needed to allow
/// correct saving of custom properties when a field is created - the custom property
/// implementation is not used by any out of box SharePoint features so is really buggy.
/// </summary>
private static Dictionary<string, string> CustomPropertiesForNewFields = new Dictionary<string, string>();

/// <summary>
/// Initialise backing fields from base property store
/// </summary>
private void InitProperties()
{
foreach (string propertyName in CustomPropertyNames)
{
CustomProperties[propertyName] = base.GetCustomProperty(propertyName) + "";
}
}

/// <summary>
/// Take properties from either the backing fields or the static store and
/// put them in the base property store
/// </summary>
private void SaveProperties()
{
foreach (string propertyName in CustomPropertyNames)
{
base.SetCustomProperty(propertyName, GetCustomProperty(propertyName));
}
}

/// <summary>
/// Get an identifier for the field being added/edited that will be unique even if
/// another user is editing a property of the same name.
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
private string GetCacheKey(string propertyName)
{
return SPContext.Current.GetHashCode() + "_"
+ (ParentList == null ? "SITE" : ParentList.ID.ToString()) + "_" + propertyName;
}

/// <summary>
/// Replace the buggy base implementation of SetCustomProperty
/// </summary>
/// <param name="propertyName"></param>
/// <param name="propertyValue"></param>
new public void SetCustomProperty(string propertyName, object propertyValue)
{
if (IsNew)
{
// field is being added - need to put property in cache
CustomPropertiesForNewFields[GetCacheKey(propertyName)] = propertyValue + "";
}

CustomProperties[propertyName] = propertyValue + "";
}

/// <summary>
/// Replace the buggy base implementation of GetCustomProperty
/// </summary>
/// <param name="propertyName"></param>
/// <param name="propertyValue"></param>
new public object GetCustomProperty(string propertyName)
{
if (!IsNew && CustomPropertiesForNewFields.ContainsKey(GetCacheKey(propertyName)))
{
string s = CustomPropertiesForNewFields[GetCacheKey(propertyName)];
CustomPropertiesForNewFields.Remove(GetCacheKey(propertyName));
CustomProperties[propertyName] = s;
return s;
}
else
{
return CustomProperties[propertyName];
}
}

/// <summary>
/// Called when a field is created. Without this, update is not called and custom properties
/// are not saved.
/// </summary>
/// <param name="op"></param>
public override void OnAdded(SPAddFieldOptions op)
{
base.OnAdded(op);
Update();
}
#endregion

public override BaseFieldControl FieldRenderingControl
{
get
{
BaseFieldControl fieldControl = new CustomFieldType1Control(this);
fieldControl.FieldName = InternalName;
return fieldControl;
}
}

public override void Update()
{
SaveProperties();
base.Update();
}

public string MyCustomProperty
{
get { return this.GetCustomProperty("MyCustomProperty") + ""; }
set { this.SetCustomProperty("MyCustomProperty", value); }
}
}
}

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.