Monday, May 26, 2008

Create a Page Layout from scratch

In this post, we will create a page layout which contains a page title and a rich html editor.

Steps:
1. Add a custom site column call Page Title.

2. Add another one called Content. There is already a site column called Page Content, we could use it, but for the purpose of this demo we will create a new one.

3. Create a custom site content type called Custom Page.

4. Since it inherits from the Page content type, it adds all the columns from page content types to the new content types automatically. Click Add from existing site columns.

5. Add those two site columns we created to this content type. If you have been following my previous
post, a content type is a collection of references to site columns as opposed to containing a collection of site columns. By using references, WSS enables reuse of site columns across multiple content types and it also ensures that changes to a site column are reflected across all content types.

6. Go to Sharepoint designer, note that the _catalogs/masterpage folder also contains page layouts for the site. Go File > New > Sharepoint Content. Whenever you create a page layout, page layout has to be bound to a content type. Select the Custom Page content type.

7. Drag and drop the content fields to the page.

8. Create a page using the page layouts created.

9. This is how it looks.


This picture shows the relationship between master page and page layouts:


References:

Page Layouts and Master Pages
MSDN Webcast: Creating a Custom Page Layout with Microsoft Office SharePoint Server 2007

Sunday, May 25, 2008

Feature Activation

There are a lot of different ways that a feature can get activated on a site.

Feature Activation via
  • Site Definition
  • Site Settings
    • Site Collection Features
    • Site Features
  • Feature.xml
    • ActivateOnDefault
    • AutoActivateInCentralAdmin
  • Activation Dependencies of other Features
  • Receiver Code
  • Feature Stapling
  • Stsadm.exe

Activation Dependencies

  • Defined in feature.xml
  • Activate one or more other features when feature is activated
  • Used to activate a group of hidden features as a unit via a single visible feature
  • Those features must be of same scope (either farm or site or web)

SPFeatureReceiver

  • Installation
    • FeatureInstalled
    • FeatureUninstalling
  • Activation
    • FeatureActivated
    • FeatureDeactivating

Saturday, May 24, 2008

List Events

List Events

  • Event handlers are registered via features
  • Can be defined in elements.xml for list types and content types
  • More commonly attached via feature receivers to specific list instances
  • Rich options for kind of events you can handle in both Lists and List Items
  • Subclass SPListEventReceiver
  • Synchronous Events by convention ending in 'ing'
    • FieldAdding, FieldUpdating, FieldDeleting
    • Synchronous events can be cancelled
  • Asynchronous Events by convention ending in 'ed'
    • FieldAdded, FieldUpdated, FieldDeleted
    • Use this to take action after an event completes

List Item Events

  • Subclass SPItemEventReceiver
  • Events
    • ItemAdding, Added
    • ItemUpdating, Updated
    • ItemDeleting, Deleted
    • ItemAttachmentAdding, Added, Deleting, Deleted
    • ItemCheckingin, CheckedIn, UncheckingOut, UncheckedOut, CheckingOut, CheckedOut
    • ItemFileMoving, Moved
    • ItemFileCoverted

Programming Lists

Lists and Libraries in the Object Model

  • SPList
  • SPDocumentLibrary
  • SPListItemCollection
  • SPListItem
  • SPQuery
  • SPSiteDataQuery

Retrieving List Instances

  • SPWeb.Lists collection
    • This returns a SPListCollection of all lists in a site
    • Can access a specific list within a collection
  • SPWeb.GetList method
    • This returns a SPList from Url of the list or any list form
  • SPWeb.GetListOfType method
    • This is a SPListCollection filtered by SPBaseType enum
  • SPWeb.GetListFromUrl, SPWeb.GetListFromWebPartPageUrl
    • From previous version of Sharepoint. Do not use. Use GetList instead.

Enumerating List Items

  • Items.GetDataTable()
    • This returns a copy of the list data as ADO.NET DataTable
    • Updates to the DataTable do not affect the list
    • Good for binding to grids
  • foreach (SPListItem item in SPList.Items)
    • Works directly against list data
    • Items are updatable
    • Provides full access to item properties

Reading and Setting Field Values

  • Read via DataTable
  • Read / Write via
    • Item[int ordinal]
    • Item[Guid File]
    • Item[string DisplayName]

Creating Lists Programmatically

  • Create SPList instance with SPWeb.Add(Title, Description, List Type)
  • Set properties
    • Quick launch
    • Security
    • Search Settings
    • Etc.
  • Call SPList.Update()

Add Items Programmatically

  • Create SPListItem instance with SPList.Items.Add()
  • Set values
    • SPListItem["field1"] = value;
  • Call SPListItem.Update();

AllowUnsafeUpdates

  • SPSecurity.RunWithElevatedPrivileges is not enough
  • Explicitly allow updates to the database without a security validation
  • When elevating privileges and updating any site or web element, you need to set
    • SPSite.AllowUnsafeUpdates = true or
    • SPWeb.AllowUnsafeUpdates = true
  • This is important because when you developing in general you will be using the administrator account. When you call RunWithElevatedPrivileges, it will work. But when you log in as somebody else who does not have access to the Lists, you will get an access denied error if you don't set the AllowUnsafeUpdates to true.

Wednesday, May 21, 2008

Import SQL table as Sharepoint List

Steps:
1. Export the SQL table into an excel file.
2. Go to Site Actions > View All Site Content > Create > Import Spreadsheet

3. Give the list a name called Location, browse the excel file to import.

4. Select the Range of Cells range type, click the _ button to select range.

5. Select the data you wanted to import and click the red arrow button.

6. Click the Import button.

7.The Location list is created.

Tuesday, May 20, 2008

MOSS Managed Property

Scenario:
Assume you have a publishing site which has a List called company which contains company information. The company list has a column named Active of type Yes/No. We want to filter the global search results to hide the inactive companies for public internet users.

Steps:
1. Create a scope called Internet User. Add a scope rule with All Content to it.
2. Create a Custom List called ListActive, add two list items 'yes' and 'no'.
3. Add a Site column called IsActive of type lookup and reference the ListActive list.
4. Add the IsActive site column to the Company List.
5. For the managed property to work, we need to use site column, so here we are replacing the Active column of Company List with IsActive site column, and delete the Active column from the company List once we've copied the data from Active to IsActive column.
6. If you only have a few companies in the Company List, you could just copy the data from Active column to the IsActive column by editing the list in an Access datasheet.
7. In this case we have hundreds of companies, I am going use the following program to do it:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SharePoint;
namespace ListSolution
{
class Program
{
static void Main(string[] args)
{
Copy_Data_To_IsActive_Column();
}
public static void Copy_Data_To_IsActive_Column()
{
SPSecurity.RunWithElevatedPrivileges(delegate()
{
SPSite rootSite = new SPSite(http://sitename/);
SPWeb web = rootSite.AllWebs[""];
SPList taskList = web.Lists["Company"];
foreach (SPListItem item in taskList.Items)
{
if (item["Active"] != null && (bool)item["Active"])
{
//this is how you update lookup column, 0 is null, 1 is yes, 2 is no
item["IsActive"] = 1;
}
else
{
item["IsActive"] = 2;

}

item.Update();

}

Console.Write("done!");

Console.ReadLine();

});

}

}

}

8. Start an Incremental Crawl of the 'Local Office SharePoint Server Sites' content source.

9. Add a new Managed Property called CompanyActive as follows:


10. Click Add Mapping, a pop up shows as follows. Choose SharePoint, find the IsActive Column,click OK.


11. Add a rule to the Internet User scope. Select Property Query then select CompanyActive property equals No. Select exclude.


12. Add the scope to the Search Core Results Web Part.

Monday, May 19, 2008

Group Data View filters

You could group data view filters by selecting the filters and click the group button to group them:



Search scope for Search Core Results Web Part

You could specify the search scope for Search Core Results Web Part:

Display Publish button for publishing portal

If you want to display the publish button without going through the Parallel Approval Workflow. Here is how:

1. Go to Modify Pages Library Settings.


2. Click Workflow settings


3. Click Parallel Approval.


4. Uncheck 'Start this workflow to approve publishing a major version of an item.'


5. Now that you are able to publish the page content without the approval workflow.


Reference:
The Publish button is not displayed when you create a site that is based on the Publishing Portal site template in SharePoint Server 2007

Tuesday, May 13, 2008

Sharepoint Inline C# Code

You could add inline C# code to a page that is created by Sharepoint designer. For example, you could show or hide a panel by the query string. Or you could handle a button click event and so on. In this case I added the following code to the NewsletterSearch.aspx page in Sharepoint designer:

<script runat="server">

protected void Page_Load(object sender, EventArgs e)

{

if (Request.QueryString["k"] == null)

{

pnlSearchResults.Visible = false;

}

else

{

pnlNewsletters.Visible = false;

}

}

</script>


Then you need to modify the application's web.config in the virtual directory folder. Under the SafeMode node, add the following:

<PageParserPaths>

<PageParserPath VirtualPath="/Newsletter/NewsletterSearch.aspx" CompilationMode="Always" AllowServerSideScript="true" />

</PageParserPaths>

Reference:

Code-blocks are not allowed in this file: Using Server-Side Code with SharePoint

Monday, May 12, 2008

Sharepoint XSLT Tips

Customize search result

  • When you reach a search result page which contains the core search web part, if you want to get rid of the auther, date as well as the file size, you can edit the web part and in the XSL editor, get rid of the following:
    • Under the template <xsl:template match="Result"> Get rid of
      • <xsl:call-template name="DisplaySize"> <xsl:with-param name="size" select="size" /> </xsl:call-template>
      • <xsl:call-template name="DisplayString"> <xsl:with-param name="str" select="author" /> </xsl:call-template>
      • <xsl:call-template name="DisplayString"> <xsl:with-param name="str" select="write" /> </xsl:call-template>
    • Get rid of the following templates:
      • DisplaySize
      • DisplayString

Customize DataView

  • If you want to hide an image that its image file name parameter of the dataview is empty, you can use:
    • <xsl:if test="contains(@ImageFileName,'.')"><img src="../Pictures/{@ImageFileName}"></img></xsl:if>
  • Say if you want to show a extract of a long description, you could just show the first sentence of the description by getting all the words before the first full stop, if there is no full stop in the description, then you can just show the first 200 letters:
    • <xsl:choose>
    • <xsl:when test="contains(@Profile,'.')">
    • <xsl:value-of select="concat(substring-before(@Profile,'.'), ' ...')" disable-output-escaping="yes"/>
    • </xsl:when>
    • <xsl:otherwise>
    • <xsl:value-of select="concat(substring(@Profile,0,200), ' ...')" disable-output-escaping="yes"/>
    • </xsl:otherwise>
    • </xsl:choose>
  • If you want to formate a date to be for example "Tuesday, 6 May 2008", here is how:
    • <xsl:value-of select="ddwrt:FormatDate(string(@Date), 3081, 3)"/>

Sunday, May 11, 2008

A Simple Search Web Part

This web part will simply implement a search function by redirecting the user to the out-of-box sharepoint search result page.

Steps:
1. Add a web custom control to the solution, name it SearchWebPart.cs.
2. Add the following namespaces:

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Linq;

using System.Text;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Web.UI.WebControls.WebParts;

3. Change the inheritance from WebControl to WebPart and delete the default Text property. Also get rid of the all the default attributes on the SearchWebPart class.

namespace SearchWebPart

{

public class SearchWebPart : WebPart

{

Label lblSearch = null;

TextBox txtSearch = null;

Button btnSearch = null;

4. Add the search textbox and search button.

protected override void CreateChildControls()

{

btnSearch = new Button();

btnSearch.Width = new Unit(50, UnitType.Pixel);

btnSearch.Text = "Search";

btnSearch.Click += new EventHandler(btnSearch_Click);

Controls.Add(btnSearch);

lblSearch = new Label();

lblSearch.Text = "Search: ";

Controls.Add(lblSearch);

txtSearch = new TextBox();

txtSearch.Width = new Unit(295, UnitType.Pixel);

Controls.Add(txtSearch);

}

5. Redirect user to the sharepoint out-of-box search result page of publishing portal and specify the Newsletters scope.

void btnSearch_Click(object sender, EventArgs e)

{

Page.Response.Redirect(string.Format(@"/Search/results.aspx?k={0}&s=Newsletters", HttpUtility.UrlEncode(txtSearch.Text)));

}

protected override void RenderContents(HtmlTextWriter output)

{

output.Write(@"<div style='width:600px; display:block; overflow:visible;' id='DIV1'>");

lblSearch.RenderControl(output);

output.Write(@"&nbsp;");

txtSearch.RenderControl(output);

btnSearch.RenderControl(output);

output.Write(@"</div>");

}

}

}

6. Add the dll to the GAC, add the safecontrol to the web.config.

7. Go to Site Settings > Web Parts > New > tick the webpart and click Populate Gallery.

8. Add the web part to the page.

Web Part Personalization

Customization

  • All users with sufficient permission to modify the web part in public shared views
  • PersonalizationScope.Shared

Personalization

  • Modifications apply to the view of the modifying user only
  • PersonalizationScope.User

Exposing Properties to Customize

  • Decorate public properties with attributes
    • Personalizable: controls personalization scope
    • WebBrowsable: causes the property to display in the web part editor
    • WebDisplayName: human readable, localized name of the property
    • WebDescription: localizable description of the property
    • Category: text used to group related properties in the web part editor

OnPreRender

  • When OnPreRender happens
    • All controls are fully loaded on the page.
    • ViewState is loaded and control values are current.
    • ViewState is not saved and can still be changed.
    • Last chance to change properties of controls before rendering take place.
  • OnPreRender Uses
    • Update control properties based on personalization properties
    • Get data from the database
    • Make web service calls
    • Connect data binding
    • Do stuff that depends on the state and values of ChildControls
    • RegisterClientScipt

Saturday, May 10, 2008

Custom Search Web Part

If you don't like look and feel of the out-of-box Search Core Result web part, you can customize it by using a xsl stylesheet through the XSL Editor of the web part. Here is how. If you want to completly replace the search result with your own custom search web part, here is how:

Steps:
1. Open VS2008, create a project. Under the Visual C# > Web > ASP.NET Server Control. Name the project CustomSearchWebPart.
2. Add the following references to the project:

  • System.Data
  • System.XML
  • Microsoft.SharePoint
  • Microsoft.Office.Server
  • Microsoft.Office.Server.Search
3. Add the following code to the control. Note here we are inheriting WebPartPages which is the sharepoint web parts rather than the ASP.NET web parts.

using System;

using System.Collections.Generic;

using System.ComponentModel;

using System.Text;

using System.Web;

using System.Web.UI;

using System.Web.UI.WebControls;

using System.Drawing;

using System.Xml;

using System.Xml.Serialization;

using System.Data;

using Microsoft.SharePoint.WebPartPages;

using Microsoft.Office.Server;

using Microsoft.Office.Server.Search.Query;

namespace CustomSearchWebPart

{

[ToolboxData("<{0}:clsSearchQuery runat=server></{0}:clsSearchQuery>")]

[XmlRoot(Namespace = "CustomSearchWebPart")]

public class clsSearchQuery : WebPart

{

[Bindable(true)]

[Category("Appearance")]

[DefaultValue("")]

[Localizable(true)]

Button cmdSearch;

TextBox txtQueryText;

Label lblQueryResult;

DataGrid grdResults;

4. Create a textbox and a search button, add them to the web part.

protected override void CreateChildControls()

{

Controls.Clear();

txtQueryText = new TextBox();

this.Controls.Add(txtQueryText);

cmdSearch = new Button();

cmdSearch.Text = "Start Search";

cmdSearch.Click += new EventHandler(cmdSearch_Click);

this.Controls.Add(cmdSearch);

lblQueryResult = new Label();

this.Controls.Add(lblQueryResult);

}

5. I am using querystring to render the search result as well, so the page which contains this web part could be called by other pages as well.

protected override void OnPreRender(EventArgs e)

{

if (!Page.IsPostBack)

{

if (Page.Request["k"] != null)

{

txtQueryText.Text = Page.Request["k"].ToString();

}

DoSearch();

}

}

private void DoSearch()

{

if (txtQueryText.Text != string.Empty)

{

keywordQueryExecute(txtQueryText.Text);

}

else

{

lblQueryResult.Text = "You must enter a search word.";

}

}

void cmdSearch_Click(object sender, EventArgs e)

{

DoSearch();

}

6. Use KeywordQuery class to get the search result into a ResultTable. Note here I have specified the search to only return result of the Newsletters search scope.

private void keywordQueryExecute(string strQueryText)

{

KeywordQuery kRequest = new KeywordQuery(ServerContext.Current);

string strQuery = strQueryText;

kRequest.QueryText = strQuery;

kRequest.HiddenConstraints = "scope:" + "\"Newsletters\"";

kRequest.ResultTypes = ResultType.RelevantResults;

ResultTableCollection resultTbls = kRequest.Execute();

if ((int)ResultType.RelevantResults != 0)

{

ResultTable tblResult = resultTbls[ResultType.RelevantResults];

if (tblResult.TotalRows == 0)

{

lblQueryResult.Text = "No Search Results Returned.";

}

else

{

ReadResultTable(tblResult);

}

}

}

7. Load the ResultTable into a DataSet.

void ReadResultTable(ResultTable rt)

{

DataTable relResultsTbl = new DataTable();

relResultsTbl.TableName = "Relevant Results";

DataSet ds = new DataSet("resultsset");

ds.Tables.Add(relResultsTbl);

ds.Load(rt, LoadOption.OverwriteChanges, relResultsTbl);

fillResultsGrid(ds);

}

8. Create a datagrid and add it to the web part. Bind it with the search result.

private void fillResultsGrid(DataSet grdDs)

{

//Instantiate the DataGrid, and set the DataSource

grdResults = new DataGrid();

grdResults.DataSource = grdDs;

//Set the display properties for the DataGrid

grdResults.GridLines = GridLines.None;

grdResults.CellPadding = 4;

grdResults.Width = Unit.Percentage(100);

grdResults.ItemStyle.ForeColor = Color.Black;

grdResults.ItemStyle.BackColor = Color.AliceBlue;

grdResults.ItemStyle.Font.Size = FontUnit.Smaller;

grdResults.ItemStyle.Font.Name = "Tahoma";

grdResults.HeaderStyle.BackColor = Color.Navy;

grdResults.HeaderStyle.ForeColor = Color.White;

grdResults.HeaderStyle.Font.Bold = true;

grdResults.HeaderStyle.Font.Name = "Tahoma";

grdResults.HeaderStyle.Font.Size = FontUnit.Medium;

grdResults.AutoGenerateColumns = false;

HyperLinkColumn colTitle = new HyperLinkColumn();

colTitle.DataTextField = "Title";

colTitle.HeaderText = "Title";

colTitle.DataNavigateUrlField = "Path";

grdResults.Columns.Add(colTitle);

BoundColumn colAuthor = new BoundColumn();

colAuthor.DataField = "Author";

colAuthor.HeaderText = "Author";

grdResults.Columns.Add(colAuthor);

grdResults.ItemDataBound += new DataGridItemEventHandler(grdResults_ItemDataBound);

grdResults.DataBind();

Controls.Add(grdResults);

}

9. Handles the ItemDataBound event of the datagrid to perform custom actions. In this case, I replace the url column with something else.

void grdResults_ItemDataBound(object sender, DataGridItemEventArgs e)

{

if (e.Item.ItemType == ListItemType.Item e.Item.ItemType == ListItemType.AlternatingItem)

{

HyperLink h = (HyperLink)e.Item.Cells[0].Controls[0];

String url = h.NavigateUrl;

if (url.Contains(@"Lists/CompanyList/DispForm.aspx?ID="))

{

url = url.Replace(@"Lists/CompanyList/DispForm.aspx", @"Pages/CompanyProfile.aspx");

h.NavigateUrl = url;

}

}

}

}

}

10. If not strong-named, then you can deploy to the bin:

  • Add the CustomSearchWebPart.dll to Inetpub\wwwroot\wss\VirtualDirectories\sitename\bin
  • Open the web.config in Inetpub\wwwroot\wss\VirtualDirectories\sitename, add the following to the SafeControls section
    • <SafeControl Assembly="CustomSearchWebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" Namespace="CustomSearchWebPart" TypeName="*" Safe="True" />

11. Create the Web Part definition file. Add the following to a notepad and name it CustomSearchWebPart.dwp:

<?xml version="1.0"?>

<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">

<Assembly>CustomSearchWebPart, Version=1.0.0.0, Culture=Neutral, PublicKeyToken=null</Assembly>

<TypeName>CustomSearchWebPart.clsSearchQuery</TypeName>

<Title>Custom Search Web Part</Title>

</WebPart>

12. Go to the page where you want to add the web part to, click to add a new web part, click the Advanced Web Part gallery and options, click the browser link and then click import to import the dwp file to the web part gallery, then add it to the page.

Reference:

Micosoft Office SharePoint Server 2007 SDK

Monday, May 5, 2008

Search Crawler Custom Workflow

In this post, I will create a custom workflow which crawls an external web url.
The scenario
  • Users need to be able to search the external web sites by adding the url to a List.
  • When a user added an item in the list, the workflow needs to be started to crawl the url so that the link will be searchable.
What the workflow will do:

  • The workflow will first create a content source of type websites and with the url as the start address.
  • Then the workflow will be set to only search the first page of the url by specifying the MaxPageEnumerationDepth to 0 and MaxSiteEnumerationDepth to 0.
  • Finally start crawling the content source created.

Steps:
1. In VS2008, create a project called CustomCrawlerWF using the Sharepoint Server Sequential Workflow template. The feature.xml and Workflow.xml will be created for you in this template.
2. Add References to Microsoft.SharePoint.dll, Microsoft.Office.Server.dll and Microsoft.Office.Server.Search.dll
3. Drag a codeActivity below the onWorkflowActivated1.
4. Double click the codeActivity1, and implement the codeActivity1_ExecuteCode event:

private void codeActivity1_ExecuteCode(object sender, EventArgs e)

{

//******************Step 1: Get the site search context*****************************

string strURL = @"http://sitename/";

SearchContext context;

using (SPSite site = new SPSite(strURL))

{

context = SearchContext.GetContext(site);

}

//******************Step 2: Get the value of the URL column of the List*****************************

String url = workflowProperties.Item["URL"].ToString();

url = url.Substring(0, url.IndexOf(','));

//******************Step 3: Get the site's content sources collection*****************************

Content sspContent = new Content(context);

ContentSourceCollection sspContentSources = sspContent.ContentSources;

if (sspContentSources.Exists(url))

{

//A content source with that name already exists

return;

}

//******************Step 4: Create a new content source and set start address and start full crawl******

WebContentSource webCS = (WebContentSource)sspContentSources.Create(typeof(WebContentSource), url);

webCS.StartAddresses.Add(new Uri(url));

webCS.MaxPageEnumerationDepth = 0;

webCS.MaxSiteEnumerationDepth = 0;

webCS.Update();

webCS.StartFullCrawl();

//******************Step 5: Put the content source into newsletter search scope******

//get hostname

if (url.StartsWith("http://"))

{

url = url.Substring(7, url.Length - 7);

if (url.Contains("/"))

{

url = url.Substring(0, url.IndexOf('/'));

}

url = "http://" + url;

}

else if (url.StartsWith("https://"))

{

url = url.Substring(8, url.Length - 8);

if (url.Contains("/"))

{

url = url.Substring(0, url.IndexOf('/'));

}

url = "https://" + url;

}

Scopes scopes = new Scopes(context);

Scope newsletters = scopes.GetSharedScope("Newsletters");

newsletters.Rules.CreateUrlRule(ScopeRuleFilterBehavior.Include, UrlScopeRuleType.HostName, url);

scopes.StartCompilation();

}


5. Click Deploy option from the Build menu in VS2008.
6. If you got an error saying "Error when you try to edit the content source schedule in Microsoft Office SharePoint Server 2007: "Access is denied",
here is the solution.
7. Go to your list and associate the workflow with the list. Set the workflow to start when a new list item is created.
8. If deploy on production, you could:
  • Copy feature.xml and workflow.xml to the 12 hive features folder.
  • Copy dll to the GAC
  • Under the 12 hive bin folder run:
    • stsadm -o installfeature -n CustomCrawlerWF
    • stsadm -o activatefeature ...

9. Note: if you edit or delete any content sources, when you add an item to a list which set this workflow, the workflow will fail. If you debug it, it will tell you the content source has been modified, so before you add the list item, just do a iisreset.

Reference:
How to: Programmatically Manage the Crawl of a Content Source
SharePoint 2007 Workflows - Writing an Ultra Basic WF