Embedding and Sharing Video in SharePoint

Introduction

The solution described here allows to embed video hosted on YouTube into SharePoint. The same approach may be applied for embedding video from another video hosting sites, for example from Vimeo.

The main idea here to store embedded video properties and not the embedded code itself.
For storing embedded video properties we will use Custom  List, see implementation section for description. Page for Video Links list (default view with embedded player)  is shown below

Usage

In order to embed a video into SharePoint:

  • On YouTube site click the Share button located under the video.
  • Click the Embed button.
  • Copy at least src attribute value provided in the expanded box (see table 1 for supported attributes).

  • Create new Video Link item  and paste attribute values for embedded code into Video Links item. Save it.

Table 1. Mapping between embedded code iframe attributes and Video Link item

Attribute name Video Links field name
src URL
width Video Width
height Video Height
allowfullscreen Allow Fullscreen
frameborder Frame Border

Video Links implementation

As was mentioned earlier only properties for embedded video code are stored and not the embedded code itself. For storing embedded video code properties is used Custom List that extends OOB Links List (TemplateType = 103)

For embedded video properties we create Video Link Content Type that inherits from Link CT (0x0105)

where we define the following fields

<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Field Type="Number"
DisplayName="Frame Border"
Required="FALSE"
EnforceUniqueValues="FALSE"
Group="Media Columns"
ID="{594e551f-180d-47ec-90e5-9195225c5932}"
StaticName="FrameBorder"
Name="FrameBorder"
Percentage="FALSE"
Hidden="FALSE"
SourceID="http://schemas.microsoft.com/sharepoint/v3">
<Default>0</Default>
</Field>
<Field Type="Number"
DisplayName="Video Height"
Required="FALSE"
EnforceUniqueValues="FALSE"
Group="Media Columns"
ID="{a1e94df1-eb6e-4fd2-aef7-bf0cc175c760}"
StaticName="VideoHeight"
Name="VideoHeight"
Percentage="FALSE"
Hidden="FALSE"
SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
<Field Type="Number"
DisplayName="Video Width"
Required="FALSE"
EnforceUniqueValues="FALSE"
Group="Media Columns"
ID="{e0406eee-7432-47d8-9080-8c1c4db23170}"
StaticName="VideoWidth"
Name="VideoWidth"
Percentage="FALSE"
Hidden="FALSE"
SourceID="http://schemas.microsoft.com/sharepoint/v3"/>
<Field Type="Boolean"
DisplayName="Allow FullScreen"
EnforceUniqueValues="FALSE"
Group="Media Columns"
ID="{b6ba6c8f-81d6-478a-a303-3b18687ec934}"
StaticName="AllowFullScreen"
Name="AllowFullScreen"
Required="FALSE"
Hidden="FALSE"
SourceID="http://schemas.microsoft.com/sharepoint/v3">
<Default>0</Default>
</Field>
<Field Type="Choice"
DisplayName="Embedding Mode"
Required="FALSE"
EnforceUniqueValues="FALSE"
Format="Dropdown"
FillInChoice="FALSE"
Group="Media Columns"
ID="{5836ef4c-c440-4cb8-a471-0ee918bfc710}"
StaticName="EmbeddingMode"
Name="EmbeddingMode"
Hidden="FALSE"
SourceID="http://schemas.microsoft.com/sharepoint/v3">
<Default>IFrame</Default>
<CHOICES>
<CHOICE>IFrame</CHOICE>
<CHOICE>Object</CHOICE>
</CHOICES>
</Field>
<Field
ID="{C1D8C50A-2146-41f6-80CC-02C7691392A3}"
Type="Computed"
Name="EmbeddedVideoOnForm"
StaticName="EmbeddedVideoOnForm"
DisplaceOnUpgrade="TRUE"
ShowInNewForm="FALSE"
ShowInDisplayForm="FALSE"
ShowInEditForm="FALSE"
ShowInFileDlg="FALSE"
DisplayName="Embedded Video"
SourceID="http://schemas.microsoft.com/sharepoint/v3/fields"
Sealed="TRUE"
Sortable="FALSE"
Filterable="FALSE">
<FieldRefs>
<FieldRef Name="URL" />
<FieldRef Name="FileLeafRef" />
<FieldRef Name="FileRef" />
<FieldRef Name="FSObjType" />
<FieldRef Name="EmbeddingMode" />
<FieldRef Name="VideoWidth" />
<FieldRef Name="VideoHeight" />
<FieldRef Name="FrameBorder" />
</FieldRefs>
<DisplayPattern>
<IfEqual>
<Expr1>
<Column Name="EmbeddingMode" />
</Expr1>
<Expr2>IFrame</Expr2>
<Then>
<HTML><![CDATA[<iframe width=']]></HTML>
<Field Name="VideoWidth"/>
<HTML><![CDATA[' height=']]></HTML>
<Field Name="VideoHeight"/>
<HTML><![CDATA[' src=']]></HTML>
<Field Name="URL"/>
<HTML><![CDATA[' frameborder=']]></HTML>
<Field Name="FrameBorder"/>
<HTML><![CDATA[' allowfullscreen']]></HTML>
<HTML>
<![CDATA['></iframe>]]>
</HTML>
</Then>
<Else>
<HTML>
<![CDATA[Not supported yet]]>
</HTML>
</Else>
</IfEqual>
</DisplayPattern>
</Field>
</Elements>

Pay attention, for rendering YouTube player Computed Field EmbeddedVideoOnForm is intended.
For rendering of a field on a List View the following XSLT style sheet is used

And finally, we create List Definition Video Links from Content Type Video Link. List schema and Template files for Video Links including whole project  may be found on GitHub.

References

  • Video Links project on GitHub
  • How to embed Vimeo player see here
  • How to embed YouTube player  see here
  • How to customize the rendering of a field in List View see here

Rendering Content Query Web Part results in Table Layout

It is known that OOB Content Query Web Part (CQWP)  renders results using Lists for arranging items as shown below

In ASP.NET WebForms for Web Control DataList there is a possibility to specify layout rendering mode using property RepeatLayout, what if the similar functionality would be available in CQWP?

So, our  goal to extend CQWP, i.e. in addition to List Layout rendering mode, lets implement functionality for rendering Content Query Web Part results in Plain Old Table Layout.
In this approach, we would like to achieve the following options:

  • Possibility to easily arrange results in columns
  • Specify items direction (horizontal or vertical)

Solution Structure

Content Query Web Part Class

using System;
using System.ComponentModel;
using System.Security.Permissions;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using Microsoft.SharePoint.Publishing.WebControls;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.WebPartPages;
namespace CQWPWithTableLayout.WebControls
{
[ToolboxItem(false)]
public class CBQTableLayout : ContentByQueryWebPart
{
#region Control Lifecycle
protected override void CreateChildControls()
{
try
{
base.CreateChildControls();
}
catch (Exception ex)
{
throw;
}
}
protected override void ModifyXsltArgumentList(ArgumentClassWrapper argList)
{
argList.AddParameter("RepeatDirection", string.Empty, RepeatDirection);
argList.AddParameter("RepeatColumns", string.Empty, RepeatColumns);
base.ModifyXsltArgumentList(argList);
}
/// <summary>
/// Return the tool panes that configure this <see cref="T:Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart"/> object..
/// </summary>
[SharePointPermission(SecurityAction.Demand, ObjectModel = true)]
public override ToolPart[] GetToolParts()
{
return new ToolPart[3]
{
new ContentByQueryToolPart(),
new WebPartToolPart(),
new CBQTableLayoutToolPart()
};
}
#endregion
#region Properties
[Category("TableLayout")]
[Personalizable(PersonalizationScope.Shared), DefaultValue(3), WebBrowsable(true)]
public int RepeatColumns { get; set; }
[Category("TableLayout")]
[Personalizable(PersonalizationScope.Shared), DefaultValue(RepeatDirection.Horizontal), WebBrowsable(true)]
public RepeatDirection RepeatDirection { get; set; }
#endregion
}
}

Tool Part Class Implementation with the ability to specify Table Layout properties

using System;
using System.Globalization;
using System.Web.UI.WebControls;
namespace CQWPWithTableLayout.WebControls
{
public class CBQTableLayoutToolPart : Microsoft.SharePoint.WebPartPages.ToolPart
{
public CBQTableLayoutToolPart()
{
Init += InitToolPart;
}
private void InitToolPart(object sender, EventArgs e)
{
Title = "Table Layout Settings";
_targetWebPart = ParentToolPane.SelectedWebPart as CBQTableLayout;
if (_targetWebPart == null)
throw new Exception("Wrong web part type error");
}
protected override void CreateChildControls()
{
CreateTableLayoutSection();
PopulateTableLayoutSection();
base.CreateChildControls();
}
public override void ApplyChanges()
{
this.ApplySettingsSectionChanges();
base.ApplyChanges();
}
private void CreateTableLayoutSection()
{
_columnsBox = new TextBox {MaxLength = 2};
_directionBox = new DropDownList();
var mainTable = new Table { CellPadding = 2, CellSpacing = 2 };
mainTable.Style["border-collapse"] = "collapse";
//Repeat Columns
AddRepeatColumnsProperty(mainTable);
//Repeat Direction
AddRepeatDirectionProperty(mainTable);
Controls.Add(mainTable);
}
private void AddRepeatColumnsProperty(Table section)
{
var rowHeader = new TableRow();
var cellHeader = new TableCell { Text = "Repeat Columns" };
rowHeader.Cells.Add(cellHeader);
section.Rows.Add(rowHeader);
var rowItem = new TableRow();
var cellItem = new TableCell();
cellItem.Controls.Add(_columnsBox);
rowItem.Cells.Add(cellItem);
section.Rows.Add(rowItem);
}
private void AddRepeatDirectionProperty(Table section)
{
var rowHeader = new TableRow();
var cellHeader = new TableCell { Text = "Repeat Direction" };
rowHeader.Cells.Add(cellHeader);
section.Rows.Add(rowHeader);
var rowItem = new TableRow();
var cellItem = new TableCell();
cellItem.Controls.Add(_directionBox);
rowItem.Cells.Add(cellItem);
section.Rows.Add(rowItem);
}
private void PopulateTableLayoutSection()
{
_columnsBox.Text = _targetWebPart.RepeatColumns.ToString(CultureInfo.InvariantCulture);
var directions = Enum.GetValues(typeof(RepeatDirection));
_directionBox.DataSource = directions;
_directionBox.DataBind();
_directionBox.Text = _targetWebPart.RepeatDirection.ToString();
}
protected void ApplySettingsSectionChanges()
{
_targetWebPart.RepeatColumns = int.Parse(_columnsBox.Text);
_targetWebPart.RepeatDirection = (RepeatDirection)Enum.Parse(typeof(RepeatDirection), _directionBox.Text);
}
private TextBox _columnsBox;
private DropDownList _directionBox;
private CBQTableLayout _targetWebPart;
}
}

Web Part manifest file

<?xml version="1.0" encoding="utf-8"?>
<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metaData>
<type name="CQWPWithTableLayout.WebControls.CBQTableLayout, CQWPWithTableLayout, Version=1.0.0.0, Culture=neutral, PublicKeyToken=f96554baea0809a2" />
<importErrorMessage>Cannot import this Web Part.</importErrorMessage>
</metaData>
<data>
<properties>
<property name="Title" type="string">Content Query (Plain Old Table Layout)</property>
<property name="Description" type="string">Displays a dynamic view of content from your site.</property>
<property name="ChromeType">TitleOnly</property>
<property name="ChromeState">Normal</property>
<property name="ItemLimit" type="int">15</property>
<property name="SortBy" type="string">{8c06beca-0777-48f7-91c7-6da68bc07b69}</property>
<property name="SortByDirection" type="Microsoft.SharePoint.Publishing.WebControls.ContentByQueryWebPart+SortDirection,Microsoft.SharePoint.Publishing,Version=14.0.0.0,Culture=neutral,PublicKeyToken=71e9bce111e9429c">Desc</property>
<property name="GroupStyle" type="string">DefaultHeader</property>
<property name="ItemStyle" type="string">Default</property>
<property name="ServerTemplate" type="string"></property>
<property name="MainXslLink" type="string" >/Style Library/XSL Style Sheets/ContentQueryMainTableLayout.xsl</property>
<property name="RepeatColumns" type="int">0</property>
<property name="RepeatDirection" type="System.Web.UI.WebControls.RepeatDirection, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">Vertical</property>
</properties>
</data>
</webPart>
</webParts>

Pay attention that we provide custom XSLT for processing of the CQWP, for more details see How to: Customize XSL for the SharePoint Content By Query Web Part.

Main XSLT style sheet for rendering results in Table Layout

<xsl:stylesheet
version="1.0"
exclude-result-prefixes="x xsl cmswrt cbq"
xmlns:x="http://www.w3.org/2001/XMLSchema"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:cmswrt="http://schemas.microsoft.com/WebPart/v3/Publishing/runtime"
xmlns:cbq="urn:schemas-microsoft-com:ContentByQueryWebPart">
<xsl:output method="xml" indent="no" media-type="text/html" omit-xml-declaration="yes"/>
<xsl:param name="cbq_isgrouping" />
<xsl:param name="cbq_columnwidth" />
<xsl:param name="Group" />
<xsl:param name="GroupType" />
<xsl:param name="cbq_iseditmode" />
<xsl:param name="cbq_viewemptytext" />
<xsl:param name="cbq_errortext" />
<xsl:param name="SiteId" />
<xsl:param name="WebUrl" />
<xsl:param name="PageId" />
<xsl:param name="WebPartId" />
<xsl:param name="FeedPageUrl" />
<xsl:param name="FeedEnabled" />
<xsl:param name="SiteUrl" />
<xsl:param name="BlankTitle" />
<xsl:param name="BlankGroup" />
<xsl:param name="UseCopyUtil" />
<xsl:param name="DataColumnTypes" />
<xsl:param name="ClientId" />
<xsl:param name="Source" />
<xsl:param name="RootSiteRef" />
<xsl:param name="CBQPageUrl" />
<xsl:param name="CBQPageUrlQueryStringForFilters" />
<xsl:param name="RepeatDirection" />
<xsl:param name="RepeatColumns" />
<xsl:variable name="BeginTable" select="string('&lt;table class=&quot;dfwp-list&quot;&gt;')" />
<xsl:variable name="EndTable" select="string('&lt;/table&gt;')" />
<xsl:variable name="BeginTableRow" select="string('&lt;tr&gt;')" />
<xsl:variable name="EndTableRow" select="string('&lt;/tr&gt;')" />
<xsl:variable name="BeginTableCell" select="string('&lt;td class=&quot;dfwp-item&quot;&gt;')" />
<xsl:variable name="EndTableCell" select="string('&lt;/td&gt;')" />
<xsl:template match="/">
<xsl:call-template name="OuterTemplate" />
</xsl:template>
<xsl:template name="OuterTemplate">
<xsl:variable name="Rows" select="/dsQueryResponse/Rows/Row" />
<xsl:variable name="RowCount" select="count($Rows)" />
<xsl:variable name="IsEmpty" select="$RowCount = 0" />
<div id="{concat('cbqwp', $ClientId)}" class="cbq-layout-main">
<xsl:if test="$cbq_iseditmode = 'True' and string-length($cbq_errortext) != 0">
<div class="wp-content description">
<xsl:value-of disable-output-escaping="yes" select="$cbq_errortext" />
</div>
</xsl:if>
<xsl:choose>
<xsl:when test="$IsEmpty">
<xsl:call-template name="OuterTemplate.Empty" >
<xsl:with-param name="EditMode" select="$cbq_iseditmode" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:call-template name="OuterTemplate.Body">
<xsl:with-param name="Rows" select="$Rows" />
<xsl:with-param name="FirstRow" select="1" />
<xsl:with-param name="LastRow" select="$RowCount" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</div>
<xsl:if test="$FeedEnabled = 'True' and $PageId != ''">
<div class="cqfeed">
<xsl:variable name="FeedUrl1" select="concat($SiteUrl,$FeedPageUrl,'xsl=1&amp;web=',$WebUrl,'&amp;page=',$PageId,'&amp;wp=',$WebPartId,'&amp;pageurl=',$CBQPageUrl,$CBQPageUrlQueryStringForFilters)" />
<a href="{cmswrt:RegisterFeedUrl( $FeedUrl1, 'application/rss+xml')}">
<img src="\_layouts\images\rss.gif" border="0" alt="{cmswrt:GetPublishingResource('CbqRssAlt')}"/>
</a>
</div>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.Empty">
<xsl:param name="EditMode" />
<xsl:if test="$EditMode = 'True' and string-length($cbq_errortext) = 0">
<div class="wp-content description">
<xsl:value-of disable-output-escaping="yes" select="$cbq_viewemptytext" />
</div>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.Body">
<xsl:param name="Rows" />
<xsl:param name="FirstRow" />
<xsl:param name="LastRow" />
<xsl:value-of disable-output-escaping="yes" select="$BeginTable" />
<xsl:choose>
<xsl:when test="$RepeatDirection = 1">
<xsl:call-template name="OuterTemplate.CallVerticalLayoutTemplate">
<xsl:with-param name="Rows" select="$Rows" />
<xsl:with-param name="FirstRow" select="$FirstRow" />
<xsl:with-param name="LastRow" select="$LastRow" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:for-each select="$Rows">
<xsl:variable name="CurPosition" select="position()" />
<xsl:call-template name="OuterTemplate.CallHorizontalLayoutTemplate">
<xsl:with-param name="CurPosition" select="$CurPosition" />
<xsl:with-param name="LastRow" select="$LastRow" />
</xsl:call-template>
</xsl:for-each>
</xsl:otherwise>
</xsl:choose>
<xsl:value-of disable-output-escaping="yes" select="$EndTable" />
</xsl:template>
<xsl:template name="OuterTemplate.CallVerticalContainerTemplate">
<xsl:param name="RowPosition" />
<xsl:param name="RowsCount" />
<xsl:param name="ColsCount" />
<xsl:if test="$RowPosition &lt;= $RowsCount">
<xsl:value-of disable-output-escaping="yes" select="$BeginTableRow" />
<xsl:variable name="CurPosition" select="$RowPosition" />
<xsl:variable name="AllRows" select="/dsQueryResponse/Rows/Row" />
<xsl:for-each select="$AllRows">
<xsl:variable name="ColPosition" select="position()" />
<xsl:choose>
<xsl:when test="$ColPosition != 1 and $ColPosition &lt;= $ColsCount">
<xsl:variable name="CurPositionAbs" select="$CurPosition + $RowsCount * ($ColPosition – 1)" />
<xsl:if test="$CurPositionAbs &lt;= (count($AllRows))">
<xsl:call-template name="OuterTemplate.CallItemTemplate">
<xsl:with-param name="CurPosition" select="$CurPositionAbs" />
</xsl:call-template>
</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:if test="$ColPosition &lt;= $ColsCount">
<xsl:call-template name="OuterTemplate.CallItemTemplate">
<xsl:with-param name="CurPosition" select="$RowPosition" />
</xsl:call-template>
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:for-each>
<xsl:value-of disable-output-escaping="yes" select="$EndTableRow" />
</xsl:if>
<xsl:if test="$RowPosition &lt;= $RowsCount">
<xsl:call-template name="OuterTemplate.CallVerticalContainerTemplate">
<xsl:with-param name="RowPosition">
<xsl:value-of select="$RowPosition + 1"/>
</xsl:with-param>
<xsl:with-param name="RowsCount">
<xsl:value-of select="$RowsCount"/>
</xsl:with-param>
<xsl:with-param name="ColsCount">
<xsl:value-of select="$ColsCount"/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.CallHeaderTemplate">
<xsl:apply-templates select="." mode="header">
</xsl:apply-templates>
</xsl:template>
<xsl:template name="OuterTemplate.CallVerticalLayoutTemplate">
<xsl:param name="Rows" />
<xsl:param name="FirstRow" />
<xsl:param name="LastRow" />
<!–Calc Cols & Rows–>
<xsl:choose>
<xsl:when test="$RepeatColumns = 0 or $RepeatColumns = 1">
<xsl:variable name="ColsCount" select="1" />
<xsl:variable name="RowsCount" select="$LastRow" />
<xsl:call-template name="OuterTemplate.CallVerticalContainerTemplate">
<xsl:with-param name="RowPosition" select="1" />
<xsl:with-param name="RowsCount" select="$RowsCount" />
<xsl:with-param name="ColsCount" select="$ColsCount" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:variable name="ColsCount" select="$RepeatColumns" />
<xsl:variable name="RowsCount" select="floor(($LastRow + $RepeatColumns – 1) div $RepeatColumns)" />
<xsl:call-template name="OuterTemplate.CallVerticalContainerTemplate">
<xsl:with-param name="RowPosition" select="1" />
<xsl:with-param name="RowsCount" select="$RowsCount" />
<xsl:with-param name="ColsCount" select="$ColsCount" />
</xsl:call-template>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="OuterTemplate.CallHorizontalLayoutTemplate">
<xsl:param name="CurPosition" />
<xsl:param name="LastRow" />
<xsl:choose>
<xsl:when test="$RepeatColumns = 0">
<xsl:if test="$CurPosition = 1">
<xsl:value-of disable-output-escaping="yes" select="$BeginTableRow" />
</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:if test="$CurPosition mod $RepeatColumns = 1">
<xsl:value-of disable-output-escaping="yes" select="$BeginTableRow" />
</xsl:if>
</xsl:otherwise>
</xsl:choose>
<xsl:call-template name="OuterTemplate.CallItemTemplate">
<xsl:with-param name="CurPosition" select="$CurPosition" />
</xsl:call-template>
<xsl:choose>
<xsl:when test="$RepeatColumns = 0">
<xsl:if test="$CurPosition = $LastRow">
<xsl:value-of disable-output-escaping="yes" select="$EndTableRow" />
</xsl:if>
</xsl:when>
<xsl:otherwise>
<xsl:if test="$CurPosition mod $RepeatColumns = 0">
<xsl:value-of disable-output-escaping="yes" select="$EndTableRow" />
</xsl:if>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="OuterTemplate.CallItemTemplateDummy">
<xsl:param name="CurPosition" />
<xsl:value-of disable-output-escaping="yes" select="$BeginTableCell" />
<xsl:value-of disable-output-escaping="yes" select="$CurPosition" />
<xsl:value-of disable-output-escaping="yes" select="$EndTableCell" />
</xsl:template>
<xsl:template name="OuterTemplate.CallItemTemplate">
<xsl:param name="CurPosition" />
<xsl:value-of disable-output-escaping="yes" select="$BeginTableCell" />
<xsl:choose>
<xsl:when test="@Style='NewsRollUpItem'">
<xsl:apply-templates select="." mode="itemstyle">
<xsl:with-param name="EditMode" select="$cbq_iseditmode" />
</xsl:apply-templates>
</xsl:when>
<xsl:when test="@Style='NewsBigItem'">
<xsl:apply-templates select="." mode="itemstyle">
<xsl:with-param name="CurPos" select="$CurPosition" />
</xsl:apply-templates>
</xsl:when>
<xsl:when test="@Style='NewsCategoryItem'">
<xsl:apply-templates select="." mode="itemstyle">
<xsl:with-param name="CurPos" select="$CurPosition" />
</xsl:apply-templates>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates select="/dsQueryResponse/Rows/Row[position() = $CurPosition]" mode="itemstyle">
</xsl:apply-templates>
<!–<xsl:value-of disable-output-escaping="yes" select="$CurPosition" />–>
</xsl:otherwise>
</xsl:choose>
<xsl:value-of disable-output-escaping="yes" select="$EndTableCell" />
</xsl:template>
<xsl:template name="OuterTemplate.CallFooterTemplate">
</xsl:template>
<xsl:template name="OuterTemplate.GetSafeLink">
<xsl:param name="UrlColumnName"/>
<xsl:if test="$UseCopyUtil = 'True'">
<xsl:value-of select="concat($RootSiteRef,'/_layouts/CopyUtil.aspx?Use=id&amp;Action=dispform&amp;ItemId=',@ID,'&amp;ListId=',@ListId,'&amp;WebId=',@WebId,'&amp;SiteId=',$SiteId,'&amp;Source=',$Source)"/>
</xsl:if>
<xsl:if test="$UseCopyUtil != 'True'">
<xsl:call-template name="OuterTemplate.GetSafeStaticUrl">
<xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.GetTitle">
<xsl:param name="Title"/>
<xsl:param name="UrlColumnName"/>
<xsl:param name="UseFileName" select="0"/>
<xsl:choose>
<xsl:when test="string-length($Title) != 0 and $UseFileName = 0">
<xsl:value-of select="$Title" />
</xsl:when>
<xsl:when test="$UseCopyUtil = 'True' and $UseFileName = 0">
<xsl:value-of select="$BlankTitle" />
</xsl:when>
<xsl:otherwise>
<xsl:variable name="FileNameWithExtension">
<xsl:call-template name="OuterTemplate.GetPageNameFromUrl">
<xsl:with-param name="UrlColumnName" select="$UrlColumnName" />
</xsl:call-template>
</xsl:variable>
<xsl:choose>
<xsl:when test="$UseFileName = 1">
<xsl:call-template name="OuterTemplate.GetFileNameWithoutExtension">
<xsl:with-param name="input" select="$FileNameWithExtension" />
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$FileNameWithExtension" />
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="OuterTemplate.FormatColumnIntoUrl">
<xsl:param name="UrlColumnName"/>
<xsl:variable name="Value" select="@*[name()=$UrlColumnName]"/>
<xsl:if test="contains($DataColumnTypes,concat(';',$UrlColumnName,',URL;'))">
<xsl:call-template name="OuterTemplate.FormatValueIntoUrl">
<xsl:with-param name="Value" select="$Value"/>
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($DataColumnTypes,concat(';',$UrlColumnName,',URL;')))">
<xsl:value-of select="$Value"/>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.FormatValueIntoUrl">
<xsl:param name="Value"/>
<xsl:if test="not(contains($Value,', '))">
<xsl:value-of select="$Value"/>
</xsl:if>
<xsl:if test="contains($Value,', ')">
<xsl:call-template name="OuterTemplate.Replace">
<xsl:with-param name="Value" select="substring-before($Value,', ')"/>
<xsl:with-param name="Search" select="',,'"/>
<xsl:with-param name="Replace" select="','"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.Replace">
<xsl:param name="Value"/>
<xsl:param name="Search"/>
<xsl:param name="Replace"/>
<xsl:if test="contains($Value,$Search)">
<xsl:value-of select="concat(substring-before($Value,$Search),$Replace)"/>
<xsl:call-template name="OuterTemplate.Replace">
<xsl:with-param name="Value" select="substring-after($Value,$Search)"/>
<xsl:with-param name="Search" select="$Search"/>
<xsl:with-param name="Replace" select="$Replace"/>
</xsl:call-template>
</xsl:if>
<xsl:if test="not(contains($Value,$Search))">
<xsl:value-of select="$Value"/>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.GetSafeStaticUrl">
<xsl:param name="UrlColumnName"/>
<xsl:variable name="Url">
<xsl:call-template name="OuterTemplate.FormatColumnIntoUrl">
<xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="cmswrt:EnsureIsAllowedProtocol($Url)"/>
</xsl:template>
<xsl:template name="OuterTemplate.GetColumnDataForUnescapedOutput">
<xsl:param name="Name"/>
<xsl:param name="MustBeOfType"/>
<xsl:if test="contains($DataColumnTypes,concat(';',$Name,',',$MustBeOfType,';'))">
<xsl:value-of select="@*[name()=$Name]"/>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.GetPageNameFromUrl">
<xsl:param name="UrlColumnName"/>
<xsl:variable name="Url">
<xsl:call-template name="OuterTemplate.FormatColumnIntoUrl">
<xsl:with-param name="UrlColumnName" select="$UrlColumnName"/>
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
<xsl:with-param name="Url" select="$Url"/>
</xsl:call-template>
</xsl:template>
<xsl:template name="OuterTemplate.GetPageNameFromUrlRecursive">
<xsl:param name="Url"/>
<xsl:choose>
<xsl:when test="contains($Url,'/') and substring($Url,string-length($Url)) != '/'">
<xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
<xsl:with-param name="Url" select="substring-after($Url,'/')"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$Url"/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="OuterTemplate.GetGroupName">
<xsl:param name="GroupName"/>
<xsl:param name="GroupType"/>
<xsl:choose>
<xsl:when test="string-length(normalize-space($GroupName)) = 0">
<xsl:value-of select="$BlankGroup"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="$GroupType='URL'">
<xsl:variable name="Url">
<xsl:call-template name="OuterTemplate.FormatValueIntoUrl">
<xsl:with-param name="Value" select="$GroupName"/>
</xsl:call-template>
</xsl:variable>
<xsl:call-template name="OuterTemplate.GetPageNameFromUrlRecursive">
<xsl:with-param name="Url" select="$Url"/>
</xsl:call-template>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$GroupName" />
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<xsl:template name="OuterTemplate.CallPresenceStatusIconTemplate">
<xsl:if test="string-length(@SipAddress) != 0">
<span class="presence-status-icon">
<img src="/_layouts/images/imnhdr.gif" onload="IMNRC('{@SipAddress}')" ShowOfflinePawn="1" alt="" id="{concat('MWP_pawn_',$ClientId,'_',@ID,'type=sip')}"/>
</span>
</xsl:if>
</xsl:template>
<xsl:template name="OuterTemplate.GetFileNameWithoutExtension">
<xsl:param name="input"/>
<xsl:variable name="extension">
<xsl:value-of select="substring-after($input, '.')"/>
</xsl:variable>
<xsl:choose>
<xsl:when test="contains($extension, '.')">
<xsl:variable name="afterextension">
<xsl:call-template name="OuterTemplate.GetFileNameWithoutExtension">
<xsl:with-param name="input" select="$extension"/>
</xsl:call-template>
</xsl:variable>
<xsl:value-of select="concat(substring-before($input, '.'), $afterextension)"/>
</xsl:when>
<xsl:otherwise>
<xsl:choose>
<xsl:when test="contains($input, '.')">
<xsl:value-of select="substring-before($input, '.')"/>
</xsl:when>
<xsl:otherwise>
<xsl:value-of select="$input"/>
</xsl:otherwise>
</xsl:choose>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
</xsl:stylesheet>

Results

Below is shown Web Part configured  to display results in 4 columns and horizontal mode layout

CBQTableLayoutHorizontal2

Visualizing organizational structure in SharePoint with Google Chart Tools

Sometimes in SharePoint we need to visualize hierarchical  data as organizational chart. Prerequisites: not to use  Flash or Silverlight technologies here.

So, in our scenario for storage of organization structure will be used SharePoint List based on Custom List , for rendering engine  Google Chart Tools.

Implementation

First, let’s take a look at list’s schema for storing organizational structure

Name Type Description
ParentOrgElement Lookup Used for parent/child relationship
OrgDescription Note Display text for chart box
OrgAdditionalProperties Note Visual behavior for chart box

From Google Charts library we’ll use Organizational Chart package only.

1. Google Charts Tools loading and package initialization

function visualizeOrgChart(orgChartProperties) {
google.load('visualization', '1', { packages: ['orgchart'] });
google.OrgChartData = orgChartProperties;
google.setOnLoadCallback(loadOrgChart);
}

view raw
visualizeOrgChart.js
hosted with ❤ by GitHub

2. Fetch data using SharePoint Web Service from list that contains organizational structure

function loadOrgChart() {
var soapEnv =
"<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'&gt; \
<soapenv:Body> \
<GetListItems xmlns='http://schemas.microsoft.com/sharepoint/soap/'&gt; \
<listName>" + google.OrgChartData.ListName + "</listName> \
<viewFields> \
<ViewFields> \
<FieldRef Name='" + google.OrgChartData.TitleFieldName + "' /> \
<FieldRef Name='" + google.OrgChartData.TooltipFieldName + "' /> \
<FieldRef Name='" + google.OrgChartData.ParentFieldName + "' /> \
<FieldRef Name='" + google.OrgChartData.StyleFieldName + "' /> \
</ViewFields> \
</viewFields> \
</GetListItems> \
</soapenv:Body> \
</soapenv:Envelope>";
$.ajax({
url: L_Menu_BaseUrl + "/_vti_bin/lists.asmx",
type: "POST",
dataType: "xml",
data: soapEnv,
complete: onOrgChartDataLoaded,
contentType: "text/xml; charset=\"utf-8\""
});
}

view raw
loadOrgChart.js
hosted with ❤ by GitHub

3. Bind data source and draw chart

function onOrgChartDataLoaded(orgChartData, status) {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Name');
data.addColumn('string', 'Parent');
data.addColumn('string', 'ToolTip');
var chartFieldNames = { Title: generateOwsFieldName(google.OrgChartData.TitleFieldName),
Tooltip: generateOwsFieldName(google.OrgChartData.TooltipFieldName),
Parent: generateOwsFieldName(google.OrgChartData.ParentFieldName),
Properties: generateOwsFieldName(google.OrgChartData.StyleFieldName)
};
var orgChartRowProperties = {};
var orgChartDataRows = [];
var orgChartResult = ($.browser.msie ? orgChartData.responseXML : orgChartData.responseText);
$(orgChartResult).find("z\\:row").each(function (rowIndex) {
var orgChartRow = [];
//if ($(this).attr(chartFieldNames.Layout) == undefined)
orgChartRow[0] = $(this).attr(chartFieldNames.Title);
//else {
// orgChartRow[0] = { v: $(this).attr(chartFieldNames.Title), f: $(this).attr(chartFieldNames.Layout) };
//}
orgChartRow[1] = ($(this).attr(chartFieldNames.Parent) != undefined ? $(this).attr(chartFieldNames.Parent).split(';#')[1] : '');
orgChartRow[2] = $(this).attr(chartFieldNames.Tooltip);
if ($(this).attr(chartFieldNames.Properties) != undefined) {
orgChartRowProperties[rowIndex] = parseChartRowProperties($(this).attr(chartFieldNames.Properties));
}
orgChartDataRows[orgChartDataRows.length] = orgChartRow;
});
data.addRows(orgChartDataRows);
$.each(orgChartRowProperties, function (rowIndex, rowProperty) {
data.setRowProperties(parseInt(rowIndex), rowProperty);
});
var chart = new google.visualization.OrgChart(document.getElementById('orgChartCanvas'));
chart.draw(data, { allowHtml: true, allowCollapse: true });
}

Usage

Result page with Org Structure rendered as Chart in List View

 

Designing and deployment of Context Data in Nintex Workflow

Context Data in Nintex Workflow represent values specific to the workflow, the context of the item and the current task within the workflow. As an example below are some of them

Initiator: The username (domain\username) who caused the item to be entered into the workflow

Item URL: The URL of the item in workflow

The complete list of Contex Data items and how to manage it is described  in Nintex Workflow User Manual.

Designing custom Context Data usually include the implementation of specific handler(class). In Nintex Workflow API  for this purposes exist class ContextDataItemBase, from which all Context Data items usually inherits.

The implementation of Context Data for returning approvers comments in XML format is presented below

/// <summary>
/// Context DataItem for All Approvers in Xml format
/// </summary>
public class AllApproverCommentsXml : ContextDataItemBase
{
/// <summary>
///
/// </summary>
/// <param name="ctx"></param>
/// <returns></returns>
public override object GetValueObject(NWWorkflowContext ctx)
{
return GetLastApproverCommentsAsXml(ctx);
}
internal string GetLastApproverCommentsAsXml(NWWorkflowContext ctx)
{
return new NintexTaskCollection(ctx.InstanceID, ctx.Web).GetAllApprovalTaskCommentsAsXml();
}
/// <summary>
/// Internal Name
/// </summary>
public override string InternalName
{
get { return "AllApproverCommentsXml"; }
}
}

Below is shown page for managing Context Data with custom Context Data added  and Insert Reference dialog that demonstrates the usage of custom Context Data when designing workflow   

Now our goal to automate the deployment of Context Data. For example, it is possible to create Data Context item during feature activation. For this task we’ll use method of class CustomCommonDataCollection (in namespace:Nintex.Workflow) from Nintex Workflow API:

public static void Add(string typeName, string assemblyName, string displayName, string description)

Adding Context Data item  in Nintex Workflow Management (shown below)

is equivalent to calling

CustomCommonDataCollection.Add(
"SharePoint.NW2010.Deployment.ContextDataItems.AllApproverCommentsXml",
"SharePoint.NW2010.Deployment, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b6acd7f49f7aeb64",
"Approver Comments (XML)",
"Approver comments in XML format");

You could grab the source code for the project that demonstrates how to provision Context Data here.

Automating deployment of reusable workflows in Nintex Workflow 2010

One of the options for creating workflows in Nintex Workflow 2010 (NW2010 for short) is to associate workflows  with Content Types.This category of workflows is known as Reusable workflows.

As stated in Nintex Workflow 2010 User Manual

A resusable workflow template allows the workflow to be used on a content type, list or library through the
default SharePoint Workflow settings option.
Reusable workflow templates can be created for use within a single site or an entire site collection

In our case we are only interested in reusable workflows for content types.

Let’s disscuss the scenario when we need to automate the deployment of reusable workflow. It may take place for example, when there are several environments. The approach discussed here allows to deploy workflows during feature activation.

NW2010 API contains publishing infrastucture (Nintex.Workflow.Publishing namespace) that allow to programatically publish workflow templates.
Class Publish contains  method

WorkflowMetaData PublishAWorkflow(string wfName, NWActionConfigurations configs, Guid listId, SPWeb web, ImportContext importCtx, bool validate, SPContentTypeId contentTypeId, string changeNotes)

view raw
PublishAWorkflow.cs
hosted with ❤ by GitHub

that allows to associate workflow using ContentTypeId parameter.

The method below demonstrate how to deploy reusable workflow file by passing it file name, contenttype name and workflow name(NWFMappingEntry parameter)  using Nintex API PublishAWorkflow method.

/// <summary>
/// Publish Reusable Workflow
/// </summary>
/// <param name="mapping"></param>
public void PublishReusableWorkflow(NWFMappingEntry mapping)
{
SPContentType ct = web.ContentTypes[mapping.BindingName];
string workflowName = mapping.WorkflowName;
string pathToNWF = Path.Combine(properties.Definition.RootDirectory, mapping.WorkflowFileName);
byte[] workflowData = File.ReadAllBytes(pathToNWF);
string workflowFile = Utility.ConvertByteArrayToString(workflowData);
while ((int)workflowFile[0] != (int)char.ConvertFromUtf32(60)[0])
workflowFile = workflowFile.Remove(0, 1);
ExportedWorkflowWithListMetdata workflowWithListMetdata = ExportedWorkflowWithListMetdata.Deserialize(workflowFile);
string xmlMessage = workflowWithListMetdata.ExportedWorkflowSeralized;
SPListCollection lists = web.Lists;
Dictionary<string, Guid> dictionary = new Dictionary<string, Guid>(lists.Count);
foreach (SPList spList in lists)
{
if (!dictionary.ContainsKey(spList.Title.ToUpper()))
dictionary.Add(spList.Title.ToUpper(), spList.ID);
}
foreach (var listReference in workflowWithListMetdata.ListReferences)
{
string key = listReference.ListName.ToUpper();
if (dictionary.ContainsKey(key) && !dictionary.ContainsValue(listReference.ListId))
xmlMessage = xmlMessage.Replace(Utility.FormatGuid(listReference.ListId), Utility.FormatGuid(dictionary[key]));
}
var exportedWorkflow = WorkflowPart.Deserialize<ExportedWorkflow>(xmlMessage);
foreach (var config in exportedWorkflow.Configurations.ActionConfigs)
WorkflowRenderer.ProcessActionConfig(config);
Guid listId = Guid.Empty;
bool validateWorkflow = true;
Publish publish = new Publish(web);
publish.PublishAWorkflow(workflowName, exportedWorkflow.Configurations, listId, web, (ImportContext)null, validateWorkflow, ct.Id, string.Empty);
}

Project that demonstrates how to deploy Nintex reusable workflows may be found here

Integrating YetAnotherForum with SharePoint

Hi

Recently i was involved in creating forum for SharePoint 2010.

Our goals were:

  • Using existing OSS forum application, in our case we’ve chosen YetAnotherForum ver. 1.9.4 (YAF for short)
  • Installation & configuration from SharePoint (WSP)

Below is brief description of solution

1)Copying forum files.
First of all our solution copy YAF files into SharePoint layouts directory structure during installation.
Note: There is no need to create separate IIS Web Site or even Application under current SharePoint Site for hosting YAF.

Some modifications need to be done in  config file for this to work, below are some of them

  • Move provider’s declarations (membership,roleManager and profile) from web.config in {hive}\layouts\yaf to   C:\inetpub\wwwroot\wss\VirtualDirectories\{Port}
  • Set  parameters YAF.FileRoot & YAF.AppRoot to  ~/_layouts/yaf

2) For solution to be truly integrated with YAF we need  mechanism to enable auto registerstration of SharePoint users in YAF (through standard YAF membership provider)

Prerequisites:

Windows authentication is used.

<authentication mode=”Windows” />
<identity impersonate=”true” />

To enable SharePoint users to be created automatically in YAF lets create YAF module

Below is full source code for module

[YafModule(“Microsoft Sharepoint To YAF Users Synchronization Module”, “vgrem”, 1)]
public class SPS2YAFUsersSyncModule : SimpleBaseModule
{
#region Constructors and Destructors
/// <summary>
/// Initializes a new instance of the <see cref=”SPS2YAFUsersSyncModule”/> class.
/// </summary>
public SPS2YAFUsersSyncModule()
{
}
#endregion
#region Public Methods
/// <summary>
/// The init forum.
/// </summary>
public override void InitForum()
{
this.PageContext.PagePreLoad += new EventHandler<EventArgs>(this.PageContext_PagePreLoad);
}
#endregion
#region Methods
/// <summary>
/// The page context_ page pre load.
/// </summary>
/// <param name=”sender”>
/// The sender.
/// </param>
/// <param name=”e”>
/// The e.
/// </param>
private void PageContext_PagePreLoad(object sender, EventArgs e)
{
Page curPage = this.PageContext.CurrentForumPage.Page;
if (
(curPage.User.Identity.AuthenticationType == “NTLM”)
|| (curPage.User.Identity.AuthenticationType == “Negotiate”)
// || ( Context.User.Identity.AuthenticationType == “Windows” )
)
{
if (CurrentYAFUserName != curPage.User.Identity.Name)
{
ProcessYAFMembershipLogin(curPage.User);
CurrentYAFUserName = curPage.User.Identity.Name;
}
}
}
/// <summary>
///
/// </summary>
/// <param name=”p”></param>
private void ProcessYAFMembershipLogin(IPrincipal p)
{
string email = p.Identity.Name + “@company.ru”;
string userName = p.Identity.Name;
int boardID = YafContext.Current.PageBoardID;
MembershipCreateStatus status;
string question = “Company”;
string answer = “Company”;
bool approved = true;
MembershipUser user;
//if(UserMembershipHelper.GetUser(false) != null)
if (!UserMembershipHelper.UserExists(userName, email))
{
// create a new user and generate a password.
int retry = 0;
do
{
string newPassword = Membership.GeneratePassword(7 + retry, 1 + retry);
user = YafContext.Current.CurrentMembership.CreateUser(
userName, newPassword, email, question, answer, approved, null, out status);
}
while (status == MembershipCreateStatus.InvalidPassword && ++retry < 10);
if (status == MembershipCreateStatus.Success)
{
// setup inital roles (if any) for this user
RoleMembershipHelper.SetupUserRoles(boardID, userName);
// create the user in the YAF DB as well as sync roles…
int? userID = RoleMembershipHelper.CreateForumUser(user, boardID);
// create profile
YafUserProfile userProfile = YafUserProfile.GetProfile(userName);
// setup their inital profile information
userProfile.Location = string.Empty;
userProfile.Homepage = string.Empty;
userProfile.Save();
YafBuildLink.Redirect(ForumPages.forum);  //Force to reload page for new user
}
}
}
#endregion
#region Properties
private string CurrentYAFUserName
{
get
{
if (HttpContext.Current.Session[“yafUserName”] != null)
return (string)HttpContext.Current.Session[“yafUserName”];
return string.Empty;
}
set
{
HttpContext.Current.Session[“yafUserName”] = value;
}
}
#endregion
}
You may create separate assembly for this module, but you must meet  some requirements for YAF engine to load your module:
  • your assembly name need start with YAF and  SPS2YAFUsersSyncModule
  • class need to be located in YAF.Modules namespace.

Now you should be able to use YAF from SharePoint

That’s all for now.
In next post we’ll take a look at some ways to improve our solution such as
  • implement Forum scopes between SharePoint sites (isolation forums for different sites)
  • create Site Definitions for forum
  • web part for using in Site page