Tuesday, February 19, 2008

Always use CAPS in content type IDs

Content type ID in the typical form looks like 0x0100[GUID], where the part shown in bold is ID of a parent content type, 00 is a separator, and [GUID] is a GUID which makes content type ID unique. A good practice is to place GUID in CAPS only. You should always do it only this way! I'll show you why it is so important in a simple example.

Consider the following: you have a content type Test Item and you need to add a button Test Action to its display form, which links to some other page. To solve this problem, you will probably create a feature, which will deploy a content type and a custom action. Let's imagine that you will define your content type without dealing with this CAPS in GUID rule:

<ContentType
ID="0x0100EB06FD49D1764bc28CFE4F0971356D39"
Name="Test Item"
Group="Test Content Types"
Description=""
Version="0"
Hidden="FALSE"
ReadOnly="FALSE"
Sealed="FALSE">
<FieldRefs>
</FieldRefs>
</ContentType>
The custom action will be defined as:
<CustomAction 
Id="TestCustomActions.DisplayFormToolbarAction"
RegistrationType="ContentType"
RegistrationId=
"0x0100EB06FD49D1764bc28CFE4F0971356D39"
Location="DisplayFormToolbar"
Sequence="100"
Title="Test Action">
<UrlAction Url="#"/>
</CustomAction>
After deploying and activating this feature, you will see a strange issue - there will be no Test Action button in the display form of Test Item. To solve this issue, you should replace content type ID in both definitions (content type and custom action) with 0x0100EB06FD49D1764BC28CFE4F0971356D39. After redeploying the feature, this issue will be solved. That's why CAPS rule makes sense!

Tuesday, October 16, 2007

Huge Issue: the Workflow Data is Purged From the SharePoint Database After 60 Days

Last week we’ve found a strange issue on one of our SharePoint servers – workflow histories on documents that were approved more than 2 months ago were missing! After some Googling I came across the following post on MSDN: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2272887&SiteID=1&mode=1.
It contains an in-detail description of this behavior. Briefly, SharePoint provides the Workflow Auto Cleanup timer job, which is enabled by default. This job performs workflow cleanup – removes references between workflow instances and list items (documents) from the database. Workflow execution history information though is not affected by cleanup procedure. It is stored in the history list for the specified workflow, which is a simple SharePoint list. Microsoft describes that this behavior is by design and by performance reasons.

However, if this behavior is not expected in your solution, you may disable Workflow Auto Cleanup job on the server. To do this, go to Central Administration -> Operations -> Timer Job Definitions. This will prevent any deletions of your workflow data.

Thursday, September 20, 2007

How to get display form URL of a list item

Often, developing custom web parts, you may want to render a link to a list item. In SharePoint 2.0 you could get a list in which a list item is stored, and just create a link pointing to its display form, providing ID of a list item as a parameter in request query string. In SharePoint 3.0 this task is a bit more complicated, because now developers can specify custom URL of edit/new/display forms for each content type. If custom URL is not specified, content type uses appropriate form of a list. To make things even more complicated, URL of a custom form can point to application pages (stored in _layouts) as well. In this case, you should pass not only ID of an item as a parameter in request query string, but also GUID of a list, in which this item is stored. Otherwise, application page can not determine in which list it should search for an item with specified ID. So, if you need to get a complete URL of item’s display form, you may code something like:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;

SPListItem item = GetItem(); // some code to get a list item
SPList list = item.ParentList;
SPWeb web = list.ParentWeb;

string webUrl = web.Url;
string dispUrl = item.ContentType.DisplayFormUrl;
if(dispUrl == "")
dispUrl = list.Forms[PAGETYPE.PAGE_DISPLAYFORM].Url;
bool isLayouts = dispUrl.StartsWith("_layouts/",StringComparison.CurrentCultureIgnoreCase);
dispUrl = String.Format("{0}/{1}?ID={2}",webUrl,dispUrl,item.ID);
if(isLayouts)
dispUrl = String.Format("{0}&List={1}",dispUrl,SPEncode.UrlEncode(list.ID+""));
As a result, you will get full display form URL of a list item. However, if you want to get just a “clickable” URL, to place in some hyperlink, you don’t need to write so much code. You may create URL by using only an address of a list display form. SharePoint will analyze provided ID of an item then and redirect your request to appropriate page. You may code just:

SPListItem item = GetItem(); // some code to get a list item
SPList list = item.ParentList;
SPWeb web = list.ParentWeb;
string webUrl = web.Url;
string dispUrl = list.Forms[PAGETYPE.PAGE_DISPLAYFORM].Url;;
dispUrl = String.Format("{0}/{1}?ID={2}",webUrl,dispUrl,item.ID);

Monday, September 17, 2007

How to modify date format in SharePoint

SharePoint out of the box provides multiple locales. You can select the one which would be used to format dates, amounts and other culture specific values in your site. However, you can’t change date format, specifying some format string. That’s why, for example, here in Latvia we are in trouble! :) The default format, what SharePoint provides for Latvia is yyyy.MM.dd. It’s correct from point of view of ISO standard. But customers here prefer using dd.MM.yyyy. That’s why, for example, in Windows this format is set during installation, overriding default ISO one for Latvian locale. There are two ways of solving this problem in SharePoint: to describe the customer why using yyyy.MM.dd. is better in his everyday work, or set German locale as default one in SharePoint. German locale contains required date format (dd.MM.yyyy), but of course it contains German names for weekdays and months as well - you will probably see these in calendar controls and schedules in your site. So the customer could choose less evil from these two. This weekend I was investigating this issue from programmer’s perspective, and had found an interesting workaround. In this post I will describe it in more detail.

The first idea that came to my mind was – ok, SharePoint uses .NET 3.0, ASP.NET controls and so on… so it probably should use CultureInfo for locales as well. So we may use CultureAndRegionInfoBuilder to create our custom locale, inheriting from Latvian. This class is described well here: http://www.codeproject.com/books/CustomCultures.asp. As you can see from this article, you cannot specify LCID for newly created locale, because LCIDs are closed technology now. BUT, SharePoint uses LCID in SPRegionalSettings to reference current locale. That’s why you cannot use custom locales with SharePoint. You can use only ones, which have valid LCID. That’s why we cannot create new locale inheriting from Latvian and use it in SharePoint site, but we should customize existing Latvian locale.

using System.Globalization;
using Microsoft.SharePoint;

static void Main(string[] args)
{
try
{
CultureAndRegionInfoBuilder.Unregister("lv-LV");
}
catch{}

CultureAndRegionInfoBuilder carib = new CultureAndRegionInfoBuilder("lv-LV", CultureAndRegionModifiers.Replacement);
carib.GregorianDateTimeFormat.ShortDatePattern = "dd.MM.yyyy";
carib.Register();
}
This code will modify ShortDatePattern parameter of Latvian locale, setting it to dd.MM.yyyy. You should reset IIS after executing the code.
Then you will see an interesting effect – the code had affected date format in date controls of edit forms, but nothing had changed in lists and display forms! Analyzing SharePoint code with Reflector I’ve found out, that SharePoint is not using .NET CultureInfo in that forms. It calls SPUtility.FormatDate method, providing current SPWeb with other parameters. This method grabs LCID from SPWeb, and goes to SharePoint COM library, asking to format specified date in specified format. This COM library of course is not using .NET CultureInfo, and that’s why it knows nothing about our changes in date format – it uses default date and time format strings for the locale having provided LCID.
I’ve tried to find out where this default short date format string is stored. As I understand, Windows has multiple possible short date format strings specified for each locale. You can see these in “Short date format” drop-down, going to Settings -> Control Panel -> Regional and Language Options -> Customize -> Date on your Windows Server 2003. If you specify some new format, it will be shown as the last one in the list of available for current culture. But it looks like SharePoint COM library is always using the first short date format available for the locale. For Latvian it will always use yyyy.MM.dd. I’ve spent lot’s of time Googling, investigating Windows registry and file system, trying to find out where these default date formats for each locale are stored. Of course I’ve found nothing – this part of Windows is not documented :)
So, the conclusion is – changes in CultureInfo on the server do affect all .NET side of SharePoint but have no effect on functionality, implemented by using COM interoperability. As I understand, COM is used while formatting dates and numbers in lists and display forms. May be it is implemented this way to improve performance. Obviously, this is a well known SharePoint problem, which you may encounter, for example, while developing custom field types. Your custom field type class will not be even instantiated, while rendering list forms. Only value stored in database and XML schema for the field will be analyzed. I’m planning to describe this in more detail in some future post. But now let’s return to modifying date display format in our SharePoint site.

If we want to see dates in the same format in all SharePoint site, we could take some existing culture as a base, and modify it, to match specific settings of our locale. SharePoint COM side will use default parameters of this culture, but .NET – customized ones. As I’ve described earlier, looks like COM side is used only for formatting date and time in short format, and numbers. You should take it in mind, while selecting a culture to use as a base one. I’ve done it using the following code:

CultureInfo lvci =  new CultureInfo("lv-LV", false);
DateTimeFormatInfo lvdtf = lvci.DateTimeFormat;
NumberFormatInfo lvnf = lvci.NumberFormat;
foreach(CultureInfo ci in CultureInfo.GetCultures(CultureTypes.AllCultures))
{
if(!ci.IsNeutralCulture)
{
DateTimeFormatInfo dtf = ci.DateTimeFormat;
NumberFormatInfo nf = ci.NumberFormat;
if(ci.LCID == lvci.LCID (dtf.ShortDatePattern == "dd.MM.yyyy" && nf.NumberDecimalSeparator == lvnf.NumberDecimalSeparator
&& nf.NumberGroupSeparator == lvnf.NumberGroupSeparator))
Console.WriteLine("{0}{1}{2}{3}{4}{5}{6}{7}{8}{9}{10}{11}{12}",
ci.LCID,
ci.Name,
ci.EnglishName,
ci.DateTimeFormat.ShortDatePattern,
dtf.TimeSeparator,
ci.NumberFormat.NumberDecimalSeparator,
ci.NumberFormat.NumberGroupSeparator,
nf.CurrencyPositivePattern,
nf.CurrencyNegativePattern,
nf.NumberDecimalDigits,
nf.NumberDecimalSeparator,
nf.NumberGroupSeparator,
nf.PercentDecimalSeparator);
}
}
As a result you’ll get a list of cultures, which have dd.MM.yyyy as a short date pattern, and number separators equal to Latvian locale. Some additional number formatting parameters will be displayed in the output, just to help you to choose more appropriate culture from the list. I’ve chosen Norwegian “nb-NO” as a base. Now executing the following code will setup this culture to be used in our site replacing Latvian locale:

try
{
CultureAndRegionInfoBuilder.Unregister("nb-NO");
}
catch{}

CultureAndRegionInfoBuilder carib = new CultureAndRegionInfoBuilder("nb-NO", CultureAndRegionModifiers.Replacement);
carib.LoadDataFromCultureInfo(new CultureInfo("lv-LV"));
carib.LoadDataFromRegionInfo(new RegionInfo("lv"));
carib.GregorianDateTimeFormat.ShortDatePattern = "dd.MM.yyyy";
carib.Register();

CultureInfo ci = new CultureInfo("nb-NO");
using(SPSite site = new SPSite("http://localhost"))
{
using(SPWeb web = site.RootWeb)
{
web.Locale = ci;
web.Update();
}
}
Finally, you should restart IIS to see the difference.

As you can see, all dates now are display as dd.MM.yyyy. But this is obvious, because now we are using Norwegian locale. The most interesting part is that calendar controls and views for lists have Latvian names for months and weekdays displayed. So now we are using Norwegian locale on our site, but all user interfaces are in Latvian!

And finally, don’t forget to setup correct sorting order and time zone in regional settings. Luckily, these are not dependant from regional settings used on the site. :)

Hope this helps!

Friday, August 31, 2007

What is good to know about SPUtility.FormatDate

Today, while developing a custom web part, I’ve decided that it would be handy to use SPUtility.FormatDate to format all dates, and avoid dealing with String.Format and CultureInfo. And it actually was! Except one small issue, it’s good to know about: SPUtility.FormatDate will not only format specified date according to regional settings used in specific web, but it also assumes that provided time is UTC one, and will convert it to local time of the web, analyzing it’s time zone.
So if you already have a local time, you should convert it to UTC first:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Utilities;

SPSite site = new SPSite("http://localhost");
SPWeb web = site.RootWeb;
DateTime dt = DateTime.Now;
string dateStr = SPUtility.FormatDate(web, web.RegionalSettings.TimeZone.LocalTimeToUTC(dt), SPDateFormat.DateTime);

Monday, August 27, 2007

Adding groups programmatically

Trying to add a group to SharePoint programmatically I’ve found an interesting issue – newly created group was not shown in Groups quick launch, but it was present in the list of all groups. After some time of analyzing SPWeb and SPGroup classes, have found an interesting point. Only groups which are associated with current web are shown in its quick launch. Other site groups are shown in the list of all groups (you can access it by clicking "More…" link in Groups quick launch). So if you want your newly created group to be accessible not only through the list of all groups, but also in Groups quick launch of some webs, you should add it to the collection of associated groups for specific web. This can be done by executing the following:

SPSite site = new SPSite("http://localhost");
SPWeb web = site.OpenWeb();
SPUser user = web.CurrentUser;
web.SiteGroups.Add("MyGroup", user, user, "My description");
SPGroup g = web.SiteGroups["MyGroup"];
web.AssociatedGroups.Add(g);
web.Update();

Don’t forget to call Update for each web, you have modified an associated groups collection to.

Tuesday, August 21, 2007

Managing web parts programmatically

Programming for SharePoint developers sometimes need to manage web parts on site pages from code. The most common tasks to perform are adding a web part, removing a web part and exporting/importing web part’s xml. In this post I’ll show how you can achieve this.

All tasks with web parts are performed through a web part manager. So first of all, you should get a web part manager for the page, you want to manage web parts in. For example:

using Microsoft.SharePoint;
using System.Web.UI.WebControls.WebParts;
using System.Collections.Generic;
using System.Xml;
using System.IO;

SPSite site = new SPSite("http://localhost");
SPWeb web = site.RootWeb;
SPFile file = web.GetFile("default.aspx");
if(!file.Exists)
{
Console.WriteLine("File not found!");
}
else
{
Microsoft.SharePoint.WebPartPages.SPLimitedWebPartManager man = file.GetLimitedWebPartManager(PersonalizationScope.Shared);
// ...
}
Now we’ve got a web part manager man, and can use it to perform all actions with web parts on default.aspx page.

To add web parts you should use AddWebPart method of the web part manager:

Microsoft.SharePoint.WebPartPages.ListViewWebPart lvwp = new Microsoft.SharePoint.WebPartPages.ListViewWebPart();
SPList list = web.Lists["MyList"];
lvwp.Title = "My ListViewWebPart";
lvwp.ListName = list.ID.ToString("B").ToUpper();
lvwp.ViewGuid = list.DefaultView.ID.ToString("B").ToUpper();
lvwp.ViewType = Microsoft.SharePoint.WebPartPages.ViewType.Html;
man.AddWebPart(lvwp,"Right",1);
In this sample, we’ve added a list view web part to the right web part zone of default.aspx page. Pay attention that ListName is actually the GUID of our list, and both ListName and ViewGuid should contain strings in upper case. Otherwise you will get an exception from ListViewWebPart class.

To remove web parts you should use DeleteWebPart method of the web part manager class:

List wpForRemove = new List();
foreach(WebPart wp in man.WebParts)
if(wp.Title+"" == "")
wpForRemove.Add(wp);
foreach(WebPart wp in wpForRemove)
man.DeleteWebPart(wp);
This code will remove all web parts with blank title from default.aspx page.

Finally, to export xml code of the web part, you can use ExportWebPart method of the web part manager class. To export all settings from your web part, you should set its ExportMode property value to All. Otherwise you would be able to export only non sensitive data, or will get an exception during export operation. To export all web parts from default.aspx page to files, you can execute:

foreach(WebPart wp in man.WebParts)
{
WebPartExportMode tempMode = wp.ExportMode;
wp.ExportMode = WebPartExportMode.All;
string fileName = String.Format(@"C:\Temp\{0}_{1}.txt", wp.ID, wp.Title);
XmlTextWriter wrt = new XmlTextWriter(fileName,Encoding.UTF8);
man.ExportWebPart(wp,wrt);
wrt.Close();
wp.ExportMode = tempMode;
}
Xml code of a web part can be used to import some web part to another page as well. To do it, just use ImportWebPart method of the web part manager, passing XmlReader with xml code of a web part as a parameter.