|
May
15
Published: May 15, 2012 06:05 AM by
Nancy Brown
Want an ajax-type grid to display tabular data in SharePoint? Try jqGrid. It's built on jQuery, easily configured, substantial documentation, sponsored by Microsoft, and there's even a "Redmond" jQuery theme which looks like it was made for SharePoint's out of the box style.
jqGrid can do a lot more than just display tabular data; it offers CRUD operations, subgrids, grouping, searching, and more. Check out the demos page for some ideas. This post is just about getting off the ground with jqGrid in SharePoint. For easy data, we'll use the venerable Northwinds sample database from an OData source. Basic steps are:
- Download jqGrid here
- Download a jQuery theme there
- Register jqGrid CSS and JavaScript with SharePoint
- Configure jqGrid
- Create a Data Adapter for jqGrid
Register jqGrid CSS and JavaScript with SharePoint
We'll build this jqGrid in a SharePoint Visual Web Part. Start with a SharePoint 2010 project containing a Visual Web Part, and add a "Layouts" Mapped Folder. It's a good idea to keep all the project stuff corralled, so the first subfolder under Layouts might well be named for the project. Then add css and js subfolders for the downloaded jQuery components.
Open the downloaded jQuery theme's css folder and copy the subfolder with your theme name to the SharePoint project's css folder. In this example, that's the "redmond" folder, including all its contents. Also from the downloaded jQuery theme, open the js subfolder, and copy the custom js file to the SharePoint project's js folder. In this example, that's the jquery-ui-1.8.20.custom.min.js file. OK, theme's done.

Now we just need four files from the jqGrid download:
- Copy to the SharePoint css folder: ui.jqgrid.css from the jqGrid download's css folder
- Copy to the SharePoint js folder:
- jquery.jgGrid.min.js and jquery-1.7.2.min.js from the jqGrid download's js folder
- grid.locale-en.js (or the language pack of your choice), from the download's js/i18n folder
Keeping things SharePoint-y, we'll register these with SharePoint constructs. To the Visual Web Part's Elements.xml file, add CustomAction elements similar to these just above the closing </Elements> tag:
<!--Register JQuery scripts-->
<CustomAction
Sequence="100"
ScriptSrc="/_layouts/Mindsharp.OData.JQuery/js/jquery-1.7.2.min.js"
Location="ScriptLink" />
<CustomAction
Sequence="110"
ScriptSrc="/_layouts/Mindsharp.OData.JQuery/js/jquery-ui-1.8.20.custom.min.js"
Location="ScriptLink" />
<CustomAction
Sequence="120"
ScriptSrc="/_layouts/Mindsharp.OData.JQuery/js/grid.locale-en.js"
Location="ScriptLink" />
<CustomAction
Sequence="130"
ScriptSrc="/_layouts/Mindsharp.OData.JQuery/js/jquery.jqgrid.min.js"
Location="ScriptLink" />
Most CustomAction elements add a link somewhere, but when the Location attribute is ScriptLink, it registers a script. The Sequence attribute is critical to getting these on the page in the right order. Be sure to edit the ScriptSrc value to match the path in your project.
To the Visual Web Part's ascx HTML source, add CssRegistration tags similar to this just below the default directives:
<SharePoint:CssRegistration runat="server"
Name="/_layouts/Mindsharp.OData.JQuery/css/redmond/jquery-ui-1.8.20.custom.css"
ID="CssRegistration1" >
</SharePoint:CssRegistration>
<SharePoint:CssRegistration runat="server"
Name="/_layouts/Mindsharp.OData.JQuery/css/ui.jqgrid.css"
ID="CssRegistration2" >
</SharePoint:CssRegistration>
Once again, the path in the Name attribute must match your project.
Configure jqGrid
This will be a very basic and minimal configuration. Check out the documentation for all the options. To the Visual Web Part's ascx source, just below the CssRegistration tags, add this html and script:
<div id="jqGrid">
<table id="Northwinds"></table>
<div id="NorthwindsPager" ></div>
</div>
<script type="text/JavaScript">
$(document).ready(function () {
$("#Northwinds").jqGrid({
url: L_Menu_BaseUrl + '/_layouts/Mindsharp.OData.JQuery/NorthwindsXML.aspx',
datatype: 'xml',
mtype: 'GET',
autowidth: true,
height: 230,
colNames: ['Customer ID', 'Company', 'Address', 'City', 'Country'],
colModel: [
{ name: 'id', width: 125 },
{ name: 'company', width: 250 },
{ name: 'address', width: 125, sortable: false },
{ name: 'city', width: 100, sortable: false },
{ name: 'country', width: 100, sortable: false }
],
pager: '#NorthwindsPager',
rowNum: 10,
rowList: [10, 20],
sortname: 'id',
sortorder: 'asc',
viewrecords: true,
gridview: true, // insert all the data at once (speedy)
caption: 'Northwinds Customers via OData'
});
// Add Navigator functions
$("#Northwinds").jqGrid('navGrid', '#NorthwindsPager',
{ edit: false, add: false, del: false, search: false });
});
</script>
The table with id="Northwinds" will be replaced by the grid (as specified by the $("#Northwinds").jqGrid script bit at the top of the script), and the div with id="NorthwindsPager" will be replaced by the navigation and paging bar below the grid (that's the pager: '#NorthwindsPager' bit near the middle of the script).
Like many jQuery-based controls, a url attribute specifies the datasource, which is the NorthwindsXML.aspx which we'll build soon. SharePoint relative urls can confuse jQuery controls, so help them out by using the SharePoint-generated javascript variable L_Menu_BaseUrl like this:
url: L_Menu_BaseUrl + '/_layouts/Mindsharp.OData.JQuery/NorthwindsXML.aspx'
Most of the configuration is self-explanatory. datatype and mtype tell jqGrid to use a GET request and expect an XML response. (JSON is another data type option.) colNames is an array of column headers for the grid. colModel defines those columns. Here, name is a unique name for the column, and will be used in the querystring, width sets a starting width (these will be adjusted to fit the parent element's width, due to the autowidth attribute also specified), and sortable determines whether that column can be sorted. Since jqGrid sends an Ajax query every time a sortable column header is clicked, or the page number is changed, or the pager dropdown is changed, or a pager button is clicked, only enable sorting on columns you'd like to code for. sortable is true by default, so turn it off where not wanted.
rowNum sets the initial number of rows to show in the grid, so it should be one of the values in rowList, which is the set of values for the number-of-rows-to-fetch dropdown in the pager bar. viewRecords: true instructs jqGrid to write the "View X to Y of Z" text on the right side of the pager bar.

sortname is the initial sort field and sortorder the initial sort direction ("asc" or "desc"). The Navigator bar is left of the pager, in the same bar. Since this is a read-only data source, all editing functions and search will be turned off by the navGrid part of the script, and only the refresh button will appear in the Navigator bar.
Create a Data Adapter for jqGrid
The grid needs a few global values returned in addition to the actual data. Specifically, it wants the number of the page returned, the total number of pages available, and the total number of records available. In XML data, jqGrid expects a "rows" root element, then page, total, and records elements, and a row element for each row of data returned. The row element should contain an id attribute (the primary index value), and cell elements for each column. CDATA sections are fine for data that needs it. Looks like this:

Since most web services don't emit this structure, we'll need to build a data adapter. Often this would be a generic HTTP handler (a .ashx file extension), but a simple aspx page will work just fine, and be a little less trouble to install in SharePoint. Add a text file to the project-named subfolder under the Layouts folder, but give it an .aspx extension; in this example, that's NorthwindsXML.aspx. Add another text file with the same name + .cs (like NorthwindsXML.aspx.cs), and Visual Studio will combine it with the .aspx added earlier. Paste something like this into the .aspx file (adapted to your project name and aspx name, of course):
<%@ Assembly Name="$SharePoint.Project.AssemblyFullName$" %>
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="NorthwindsXML.aspx.cs" Inherits="Mindsharp.OData.JQuery.NorthwindsXML" %>
Since we're only sending back XML, we don't need a lot of directives. To the code behind file, (NorthwindsXML.aspx.cs in this example), add something like:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Xml.Linq;
using System.Collections.Specialized;
namespace Mindsharp.OData.JQuery
{
public partial class NorthwindsXML :
System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
Response.ContentType = "text/xml";
Response.Write(CreateXML());
}
}
The Northwinds database is published as an OData feed courtesy of OData.org. If you've ever used ListData.svc in SharePoint, (example: http://intranet/_vti_bin/ListData.svc), you've used an OData feed. If OData is new to you, look into http://odata.org and Query SharePoint Foundation with ADO.NET Data Services. Basically it is entity classes served up in an Atom Pub style RSS feed, queryable by URL - it's REST for SharePoint List data. But back to the Northwinds OData - add a service reference to http://services.odata.org/Northwind/Northwind.svc, and give the proxy class a namespace- in this example it's NorthwindProxy.

Now we can create some XML from our OData feed. This particular feed is read-only, and has a server-side page limit of 20 items. For simplicity, we'll stay within that limit with our jqGrid configuration. Here's the CreateXML method:
private string CreateXML()
{
NorthwindProxy.NorthwindEntities ctx =
new NorthwindProxy.NorthwindEntities(
new Uri("http://services.odata.org/Northwind/Northwind.svc"));
int totalRecords = (from c in ctx.Customers
select c).Count();
// Initialize variables to contain validated query string parameters
string sortField = default(string);
string sortDirection = default(string);
int maxRowsToReturn = default(int);
int pageToReturn = default(int);
// Computed variable to send back to jqGrid
int totalPages = default(int);
CreateValuesFromQuerystring(Request.QueryString, totalRecords,
out sortField, out sortDirection,
out maxRowsToReturn, out pageToReturn, out totalPages);
var query = CreateQuery(ctx, sortField, sortDirection,
maxRowsToReturn, pageToReturn, totalRecords);
var customerQuery = from c in query
select new
{
c.CustomerID,
c.CompanyName,
c.Address,
c.City,
c.Country
};
XElement xrows = new XElement("rows",
new XElement("page", pageToReturn),
new XElement("total", totalPages),
new XElement("records", totalRecords));
foreach (var c in customerQuery)
{
xrows.Add(
new XElement("row", new XAttribute("id", c.CustomerID),
new XElement("cell", c.CustomerID),
new XElement("cell", c.CompanyName),
new XElement("cell", c.Address),
new XElement("cell", c.City),
new XElement("cell", c.Country))
);
}
// XDocument does not provide the XML declaration, so here it is
string xmlDeclaration = "<?xml version =\"1.0\" encoding=\"utf-8\"?>"
+ Environment.NewLine;
return xmlDeclaration + xrows.ToString();
}
The CreateValuesFromQuerystring() method reads the querystring and guarantees that all the variables harvested from it will have a valid value. A typical query for this project may look like this:
GET /_layouts/Mindsharp.OData.JQuery/NorthwindsXML.aspx?_search=false&nd=1337027722102&rows=10&page=2&sidx=id&sord=asc
Note that jqGrid throws in a Unix-style timestamp (nd=1337027722102), which helps defeat inadvertent browser caching.
Linq allows chaining of queries- meaning a single query can be built up in multiple statements, and a query can be the subject of another query. Take a look at these methods which build the Linq query dynamically by chaining:
private IQueryable<NorthwindProxy.Customer> CreateQuery(
NorthwindProxy.NorthwindEntities ctx,
string sortField, string sortDirection,
int maxRowsToReturn, int pageToReturn, int totalRecordsAvailable)
{
var query = from customer in ctx.Customers
select customer;
query = AddOrderBy(query, sortField, sortDirection);
query = query.Skip(maxRowsToReturn * (pageToReturn - 1))
.Take(maxRowsToReturn);
return query;
}
private IQueryable<NorthwindProxy.Customer> AddOrderBy(
IQueryable<NorthwindProxy.Customer> query, string field, string direction)
{
if (direction == "asc")
{
switch (field)
{
default:
case "id":
query = query.OrderBy(c => c.CustomerID);
break;
case "company":
query = query.OrderBy(c => c.CompanyName);
break;
}
}
else
{
switch (field)
{
default:
case "id":
query = query.OrderByDescending(c => c.CustomerID);
break;
case "company":
query = query.OrderByDescending(c => c.CompanyName);
break;
}
}
return query;
}
There are cooler ways to dynamically add the OrderBy clause, but we're keeping things simple. For the curious, see Scott Guthrie's post on Dynamic Linq queries.
That should take care of the data adapter for the grid, so it's ready to deploy. Add the Web Part to a page and enjoy! |
Apr
11
Published: April 11, 2012 06:04 AM by
Nancy Brown
Scenario: SQL Server 2012 Reporting Services (SSRS) is installed in SharePoint integrated mode. A Visual Studio 2010 Report Server Project deploys Reports and a Shared Data Source to SharePoint 2010 Document Libraries "Dashboard" and "Data Connections". Everything works as expected. Then the SharePoint Library "Data Connections", (which contains the deployed Shared Data Source "TimeBillingDataSource"), is deleted. A new SharePoint Library is created to replace it, on the same Web, with the same name, same Content Type, same everything. The untouched Visual Studio project is redeployed, creating again the same Shared Data Source in a SharePoint Library with the same name, on the same Web. But the Reports in the "Dashboard" Library employing that Shared Data Source are broken.
Here's the new Library and new Shared Data Source:
Clicking either report name in the Dashboard Library provokes this error message:
The report server cannot process the report or shared dataset. The shared data source 'TimeBillingDataSource' for the report server or SharePoint site is not valid. Browse to the server or site and select a shared data source.
That's because the link is missing.
But it can be repaired. Click the down-arrow next to a Report name to reveal the context menu, and choose "Manage Data Sources".
On the Manage Data Sources page, click the name of the Data Source…
…which leads to a page to manage the actual Shared Data Source, where the Link is missing. Click the ellipsis button (the button with three dots on it)…
...and use the Select an Item dialog to find that newly created Shared Data Source in the Data Connections library.
Click OK enough times to get that cemented into place, and then go test that Report.
Much better! This process must be repeated for each report that uses the restored Shared Data Source. Another way to fix this for the reports in the Visual Studio project: define a new Shared Data Source with a different name, but the same characteristics. Then for each report that used the deleted Data Source, open the report, and replace the report's Data Source with the new one, then deploy everything.
Why did the link break when the names are the same? Typically SharePoint identifies everything with a GUID, and this is no exception. The ID changed when the Shared Data Source was deployed the second time, and that's when the links went missing.
Here's the ID before the Data Connections library was deleted, as seen by poking around in the Reporting Service database in SQL Server Management Studio:
Then the SharePoint Library containing the Shared Data Source was deleted and recreated, and the Visual Studio project was re-deployed…
------ Deploy started: Project: Mindsharp.Reports.Dashboard, Configuration: Debug ------
Deploying to http://intranet/
Deploying data source 'http://intranet/Management/Data Connections/TimeBillingDataSource.rsds'.
Deploying report 'http://intranet/Management/Dashboard/Hours Summary.rdl'.
Deploying report 'http://intranet/Management/Dashboard/Hours Detail.rdl'.
Deploy complete -- 0 errors, 1 warnings
========== Build: 1 succeeded or up-to-date, 0 failed, 0 skipped ==========
========== Deploy: 1 succeeded, 0 failed, 0 skipped ==========
…which leads to a new ID and missing links in the Report Server database:
Note that the first deployment task is to deploy to the Report Server itself, then to SharePoint.
Hope that helps! |
Feb
29
Published: February 29, 2012 14:02 PM by
Nancy Brown
|
What is a Module? According to the Bing dictionary, the first definition is:
- self-contained interchangeable unit: an independent unit that can be combined with others and easily rearranged, replaced, or interchanged to form different structures or systems
In SharePoint, <Module> is the part of CAML (Collaborative Application Markup Language) that provisions files into the Content Database. It has one permissible child element: <File>. So maybe it could have been more mnemonically named <FileProvisioner> or <AddTheseFilesToTheContentDatabase>. However, it does rearrange an independent unit (a file) to form different structures.
<Module> comes in two flavors: Module Element (Module) and Module Element (Site). They have the same set of attributes, and the same single child element: <File>. The difference is where each is found, and the starting location for the Module's Path attribute. Module Element (Module) is found in a Feature's Elements.xml, and accordingly the Path attribute is relative to {SharePointRoot}\TEMPLATE\FEATURES\FeatureName. Module Element (Site) is found in a Site Definition's Onet.xml, and the Path attribute is relative to {SharePointRoot}\TEMPLATE\SiteTemplates\Site_Definition.

Modules can rename, repurpose, and provision one physical file to many locations
This post will focus on Module Element (Module) - the one found in Feature xml - but the concepts are the same for both flavors. For simplicity, Farm Solution Features will be the target of the initial discussion - it is easier to see where the physical files are when they're on the file system. The essence of a Module Element (Module) is taking a file found in its Feature's folder structure, and provisioning that file into the Content Database, so that it becomes a file in an SPWeb's folder or in an SPWeb's list/library.
Why is a file "provisioned" rather than "copied"? When the Module provisioning process deposits a file into the database, it deposits an SPFile object. When that SPFile is first created (provisioned), it is designated "uncustomized", meaning it works like a pointer to the actual physical file on the Web Front End (WFE) server's file system - the file in the Feature's folder. In Module provisioning, a single physical file can be virtually renamed, provisioned to multiple locations, and properties can be set, or objects added, depending on what kind of file it is. If the file is an aspx page, navigational elements or web parts can be added. If it is a webpart file, properties can be set. Consider this somewhat contrived illustration:

In the illustration above, a Module takes a single web part page (BasicLeftRightZones.aspx) and renames, repurposes, and provisions it to five different locations in a web. This is really awesome for scaling purposes. There's just one physical copy of BasicLeftRightZones.aspx, and if that physical copy is changed, the changes are propagated everywhere. However, often a single file just gets provisioned to a single location, but since this is part of Feature deployment, provisioning might happen many times, on multiple Sites or Webs. What if one of those Sites/Webs needs to alter such a file?
Customized or Uncustomized, that is the question
Imagine that a Feature named "AlphaApp" contains a Module which provisions a file named "Alpha.aspx", and the AlphaApp Feature has been activated on 3,274 sites. In the database, that's 3,274 "uncustomized" SPFile objects which are pointers to the one physical "Alpha.aspx" in the {SharePointRoot}\TEMPLATE\Features\AlphaApp folder. Then on one of those 3,274 sites, the "Management" site, someone uses SharePoint Designer to alter the source of Management's Alpha.aspx. The Management site's Alpha.aspx SPFile is now "customized", and no longer functions like a pointer to the file system; instead, the customized Alpha.aspx source is now actually in the database. The SPFile class has a property, CustomizedPageStatus, which indicates whether the file's status is currently "uncustomized" (and cached), "customized" (and not cached), or "none" (never cached). "Cached" means cached in memory on the WFE server. What's with that caching? It goes hand-in-hand with the customized page status. The SPFile class also has method RevertContentStream() which returns the file to its uncustomized state.
Files can be uncustomized in SharePoint Designer, in code, and through the browser by choosing Site Actions > Site Settings > Reset to Site Definition. This last option lands on the "Reset Page to Site Definition Version" page, where one can select one file to revert, or the entire site. Charmingly, the actual webpage name there is "reghost.aspx", which is a reference to the old SharePoint way of labeling SPFile files as ghosted or unghosted. In SharePoint 2010 terms, ghosted = uncustomized, unghosted = customized.

FROM and TO
If a Module Element (Module) provisions files from a Feature folder to the database, it must have something like a FROM and a TO. Or maybe the <File> element has the FROM and TO. Confusingly, both <Module> and <File> elements have FROM and TO bits.
Modules elements have a bunch of attributes, but the ones primarily involved in the FROM/TO business are these: Url, Path, and SetupPath. Similarly, the File element has attributes Url, Path, and Name. Some of these do double duty, sometimes acting as a FROM, sometimes acting as a TO. What attributes are required? For a Module, the Name attribute is required; for a File, the Url attribute is required. Let's explore some examples, all based on a web whose Url is http://intranet. The simplest Module could look like this:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="Simple">
<File Url="Sample.aspx" />
</Module>
</Elements>
In the above example, the File element's Url attribute is both the FROM and the TO. This CAML code says that Sample.aspx is in the Feature's root folder, and will be provisioned to the Web's root folder. After provisioning, one could browse to http://intranet/Sample.aspx. However, this is an uncommon configuration; in fact, one cannot build this in Visual Studio, since it will not allow files to be added to the Feature's root folder. (There are examples of the Url attribute being both FROM and TO and files living in the Feature's root folder in the built-in Features, such as the BasicWebParts Feature.)
Since the Url attribute is required for the File element, it will always function as either the FROM or the TO (or both, as above). In the File element, if the Name attribute is added, Name becomes the TO, thus making Url the FROM. If instead the Path attribute is included, Path becomes the FROM, thus making Url the TO. (See why this is confusing?) For the rest of the code examples, just the Module and File elements will be shown. Here's an example using just Url and Name:
<!-- FROM: Url TO: Name-->
<Module Name="Simple">
<File Url="Sample.aspx" Name="Alice.aspx" />
</Module>
Now one could browse to http://intranet/Alice.aspx. The Sample.aspx file has been renamed Alice.aspx and placed in the web's root folder.
How about this:
<!-- FROM: Url TO: Name-->
<Module Name="Simple">
<File Url="Sample.aspx" Name="Books/Alice.aspx" />
</Module>
Now one could browse to http://intranet/Books/Alice.aspx - the Sample.aspx file has been renamed Alice.aspx, and a folder (Books) has been automatically provisioned to contain it. How about this:
<!-- FROM: Path TO: Url-->
<Module Name="Simple">
<File Url="Books/Alice.aspx" Path="BookPages\Sample.aspx" />
</Module>
One could still browse to http://intranet/Books/Alice.aspx, but the original file is now found in the BookPages folder under the Feature folder. This is typically what one would see in Visual Studio, except that it would default the Url attribute to BookPages/Sample.aspx. What if one tried using Path, Name, and Url in the same File element?
<Module Name="BookPages">
<!-- FROM: Path TO: Name (Url is ignored) -->
<File Path="BookPages\Sample.aspx"
Url="Books/Alice.aspx"
Name="Books/Fiction/Alice.aspx"/>
</Module>
Name wins! This will provision Alice.aspx and two nested folders, so that one can browse to http://intranet/Books/Fiction/Alice.aspx. The Url attribute is ignored - but remember it cannot be removed, it is required, and it cannot be empty. Clearly this is not an optimal situation; a better practice would be choosing Url+Name attributes or Url+Path attributes, but not all three.
A couple of other attributes for the File element deserve special attention. Contemplate the attribute IgnoreIfAlreadyExists. Sounds like that means one can overwrite an already provisioned file, but nope, that's wrong. IgnoreIfAlreadyExists is Boolean, and if set to True, the Module will not throw an exception if it encounters an existing file. If IgnoreIfAlreadyExists is not present, or is set to False, and the Module encounters an existing file, it will throw an exception. So, if you'd rather not be notified that the Module failed to provision a file because the file was already there, set IgnoreIfAlreadyExists="True". Maybe it should be renamed DontThrowExceptionIfFileAlreadyExists.
<Module Name="BookPages">
<!-- FROM: Path TO: Name (Url is ignored) -->
<File Path="BookPages\Sample.aspx"
Url="."
Name="Books/Fiction/Alice.aspx"
IgnoreIfAlreadyExists="TRUE">
</File>
</Module>
Why would the file already be there? Because Modules do not clean up behind themselves; they provision when the Feature is activated, but do not un-provision when it deactivates. Typically the FeatureDeactivating method is used to clean up files left lying around by Modules. Then the coast is clear for a new provisioning. Of course, if the business user has made customizations through the browser, those will be lost - so there may be times when that mysterious IgnoreIfAlreadyExists="True" attribute is useful.
Visual Studio quietly takes care of deployment conflicts (like the file already exists) by default. Watch the Output window when deploying a Module Feature the second time, and you'll see something like "Found 1 deployment conflict(s). Resolving conflicts ...Deleted file 'http://intranet/Books/Fiction/Alice.aspx' from server." This is great for development, but be aware that in production, re-activating a Feature with a Module will throw an exception if there's been no cleanup of those files orphaned by the Module. This Visual Studio behavior can be controlled by the Deployment Resolution Conflict property on a Module's folder. In Solution Explorer, select the Module folder, then peek in the Properties window.
GhostableInLibrary and Ghostable
Let's look at provisioning to a List - a specialized List known as a Document Library in this case:
<Module Name="BookPages" List="101" Url="Docs">
<!-- FROM: Path TO: Url+Url -->
<File Path="BookPages\Sample.aspx"
Url="Alice.aspx"
Type="GhostableInLibrary"/>
</Module>

The File element has sprouted another attribute, Type="GhostableInLibrary". If this is left off, the file will not provision to the Library. Remember the old SharePoint terms "ghosted" and "unghosted"? Yep, this hearkens back to those older terms, and specifies that the file provisions to a Library, and is cached. The List attribute in the Module element is not required, but it can help alert one to a missing library. Without it, SharePoint will merrily create a folder if no such library exists at the Module's Url. With it, SharePoint will throw an exception if there's no library. (A spurious folder when one expected a Library can be hard to troubleshoot.) The number 101 in List="101" is the List Template Type. For a listing of List Template Types, see SPListTemplateType Enumeration.
The final Url is now a concatenation of the Module's Url attribute (which is the List's Url) and the File's Url attribute (which is the final name of the file), so one can browse to http://intranet/Docs/Alice.aspx, and see the file in the Library Docs. The Module element can also have a Path attribute, which prefixes any Path attribute in the File element.
Take a look at this variation which uses the Path attribute in the Module element to provision a folder in the Library, and stuffs Alice.aspx in it, resulting in a Url of http://intranet/Docs/Books/Alice.aspx:
<Module Name="BookPages" List="101" Url="Docs/Books" Path="BookPages">
<!-- FROM: Path+Path TO: Url+Url -->
<File Path="Sample.aspx"
Url="Alice.aspx"
Type="GhostableInLibrary"/>
</Module>

But what about the Type=Ghostable attribute?
<Module Name="BookPages" List="101" Url="Docs">
<!-- FROM: Path TO: Url+Url -->
<File Path="BookPages\Sample.aspx"
Url="Alice.aspx"
Type="Ghostable"/>
</Module>
Switching to Type="Ghostable" creates a "Docs" folder at http://intranet/Docs, with Alice.aspx inside it, and now again one can browse to http://intranet/Docs/Alice.aspx. Since there's already a Library at http://intranet/Docs, this could be seriously confusing.

SetupPath, Sandboxed Solutions, and the Visual Studio view
There's one more Module attribute to mention: SetupPath. Pick Path OR SetupPath, but not both. SetupPath is the physical path to a folder which is relative to {SharePointRoot}\TEMPLATE, so it's a way to reference files in folders there or below. If you inadvertently include both, SetupPath will win. Want to grab Default.aspx from the Team Site definition and add it to the Docs Library as Nathan.aspx?
<Module Name="BookPages" List="101" Url="Docs" SetupPath="SiteTemplates/STS">
<!-- FROM: SetupPath+Path TO: Url+Url -->
<File Path="Default.aspx"
Url="Nathan.aspx"
Type="GhostableInLibrary"/>
</Module>

So far, this has all been based on Farm Solutions, which write to the {SharePointRoot}\TEMPLATE\FEATURES\ folder, because one can see the Feature structure there easily. Recall that the Path attribute in a Module Element (Module) is relative to the {SharePointRoot}\TEMPLATE\Features\FeatureName folder. Modules work the same way in Sandboxed Solutions, but the Feature Folder structure is inside the WSP.
Switching a project from Farm Solution to Sandboxed Solution needs a little care, as Visual Studio is caching a bunch of stuff. First deactivate all Features in the Solution, then retract the Farm Solution, which removes the files from the {SharePointRoot}\TEMPLATE\FEATURES\ folder, the GAC, etc. Flip the Project's Sandboxed Solution property to TRUE, and close Visual Studio, flushing the cache. Reopen Visual Studio and redeploy. Now the WSP is in the Solutions Gallery. On the site, choose Site Actions > Site Settings > Solutions to navigate to the Solutions Gallery, or just open Visual Studio's bin/debug folder in Windows Explorer to see it there. Peering inside the WSP is easy with the Expand command.
In a command prompt window, type:
expand [PathToWSP] -f:* [PathToDestinationFolderForExpandedWSP]
The -f:* switch says expand all the folders and files inside the WSP. The Destination folder must already exist.

The Feature structure inside the Cabinet file, (the WSP), is the same as it would be in the {SharePointRoot}\TEMPLATE\FEATURES folders for a Farm Solution:

This is not so easy to see in the Solution Explorer in Visual Studio:

Bear in mind that when deployed, all those Visual Studio folders containing Elements.xml files will end up inside a Feature folder. There's more to Modules, but I hope this has de-mystified the basics. Happy coding! |
Jan
25
Published: January 25, 2012 16:01 PM by
Nancy Brown
|
What's a concurrency conflict? Picture this: Zach fires up a WPF application that does CRUD operations on a SharePoint List, the app loads a bunch of List Items, then Zach pauses to answer the phone. Two seconds later, Rebekah browses to that same SharePoint List, and updates Item 3. Zach gets off the phone, updates the same item in the same list, and clicks the Save button. (There was a little office miscommunication.) The WPF app detects a concurrency conflict - the item it is trying to update has changed since the app read it from the server.
How are these concurrent updates handled? Typically there are two kinds of concurrency options: pessimistic and optimistic. Pessimistic concurrency involves locking the entity being updated, so that nothing else can change it until after the current update operation finishes. Optimistic concurrency does not lock the entity being updated, so it is possible that the entity on the server changed in the time between reading it from the server's data source and attempting the update, resulting in a concurrency conflict.
SharePoint REST supports optimistic concurrency, and monitors concurrency conflicts by ETags, which are sort of like version numbers. (ETags, or Entity Tags, are part of the HTTP protocol: http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11) When the ETag in a WPF application (or Windows Forms or Silverlight or …) does not match the ETag in the REST service, that's a concurrency conflict. Sometimes changes should be pushed through, even if such conflicts exist. In SharePoint REST, there are at least a couple of different persist-in-spite-of-concurrency-conflicts scenarios that might occur.
The first scenario occurs when there are detached entities- entities that are not part of a Data Context collection- the client ETag can be set to * with an overload of the Data Context AttachTo method. The REST service will interpret the * ETag from the client as a "we don't care about any concurrency conflicts" message, and do the update. Detached entities typically occur when there is no query filling the collection. Instead, perhaps a list of known IDs is passed to some method, and it creates entities for existing List Items, then attaches them to a Data Context collection for updating purposes. That could look something like this for a SharePoint List called ProjectMembers:
OperationsDataContext ctx = new OperationsDataContext(
new Uri("http://intranet/operations/_vti_bin/ListData.svc"));
ctx.Credentials = CredentialCache.DefaultCredentials;
ProjectMembersItem pmi = ProjectMembersItem.CreateProjectMembersItem(10);
pmi.ProjectID = 4;
// Set various properties...
ctx.MergeOption = MergeOption.OverwriteChanges;
ctx.AttachTo("ProjectMembers", pmi, "*");
ctx.UpdateObject(pmi);
try
{
ctx.SaveChanges();
}
catch (Exception ex)
{
// network conditions, server availability...
}
The Data Context's MergeOption setting determines what happens when the SaveChanges method is called. Merge option OverwriteChanges causes all current values in the client collection, (ProjectMembers in this example), to be overwritten with fresh data from the REST service, effectively syncing what's already in the local collection with the data source. This overload of the AttachTo method adds a detached entity to the Data Context collection, and sets the ETag value. Since the Data Context was not tracking this entity before, (but it is now), UpdateObject is called to explicitly set the freshly attached entity's state to Modified.
Note that the DataContext method AttachTo is used for existing entities, which can be modified or deleted. In the proxy classes created by adding a service reference, there are methods with the naming convention AddToListName, like AddToProjectMembers for the ProjectMembers List. These methods are wrappers for the Data Context's AddObject method, which creates a brand new entity in the Data Context collection, ready to be inserted in the data source.
The second scenario involves a Data Context collection that has been filled by a query. In that case, persisting updates in spite of concurrency conflicts looks a bit different. Consider this demo WPF application, which updates a SharePoint List called Members; just a DataGrid, a button, and a TextBlock.
Here's the initial load code, with the query using a view projection to grab just two columns for updating purposes:
OperationsDataContext ctx = null;
IQueryable<MembersItem> query = null;
DataServiceCollection<MembersItem> members = null;
public MainWindow()
{
InitializeComponent();
ctx = new OperationsDataContext(new Uri("http://intranet/operations/_vti_bin/ListData.svc"));
ctx.Credentials = CredentialCache.DefaultCredentials;
query = from m in ctx.Members
select new MembersItem
{
MemberName = m.MemberName,
LoginName = m.LoginName
};
members = new DataServiceCollection<MembersItem>(query);
dataGrid1.ItemsSource = members;
}
When this first loads, the Data Context begins tracking all the entities loaded by the query, which means they have ETags already, they are already attached to the collection, and their state is Unchanged. Since the Data Context collection provides change tracking, the entity state for each item will be automatically updated to Modified, Deleted, or Added as the business user makes changes in the WPF application. In order for any of these updates to be applied by the SharePoint REST Service, the ETags in this local collection have to match the ETags in the service.
When a concurrency conflict occurs in this situation, it is possible to reload the local collection from the REST service, updating the ETags, but preserving the changes that were made in the client, so the ETags in the client collection match the service ETags, and the update can be tried again. Unless another concurrency conflict occurs, (not likely, but possible), a second call to SaveChanges() after that reload will probably push all changes through, except for Delete operations on already-deleted entities. If that WPF app attempts to delete an entity, but somebody else already deleted it on the server, the delete operation can fail on both first and second attempts, leaving the deleted entity hanging around in the local collection. So, after a second attempt to push through updates, it may be reasonable to just refresh the local collection.
The Data Context's MergeOption controls how queries load the local collection, as well as how SaveChanges updates that local collection. It is set to a value in the enumeration System.Data.Services.Client.MergeOption: AppendOnly, PreserveChanges, OverwriteChanges, NoTracking. The Save Changes and Refresh button code from the WPF app demonstrates this:
private void saveButton_Click(object sender, RoutedEventArgs e)
{
try
{
// If all updates complete successfully, a response object will be returned
DataServiceResponse response = ctx.SaveChanges(SaveChangesOptions.ContinueOnError);
this.textBlock1.Text = "Update successful!";
}
catch (DataServiceRequestException dse)
{
// If any update fails, the response object can be found
// as a property of the DataServiceRequestException
foreach (ChangeOperationResponse cor in dse.Response)
{
// if details are needed, process each ChangeOperationResponse
}
// Try to persist updates, ignoring concurrency conflicts
// Execute query again, getting fresh data from OData service,
// preserving any uncommitted changes in the local collection (members),
// and refreshing ETags
ctx.MergeOption = MergeOption.PreserveChanges;
members = new DataServiceCollection<MembersItem>(query);
this.dataGrid1.ItemsSource = members;
try
{
DataServiceResponse response = ctx.SaveChanges(SaveChangesOptions.ContinueOnError);
this.textBlock1.Text = "All updates persisted";
}
catch (DataServiceRequestException dse2)
{
// It is unlikely, but possible, that more concurrency conflicts occurred.
// However, an attempt to delete a List Item that is already deleted
// in SharePoint will create the DataServiceRequestException - but isn't
// really an issue, since the end result (deletion) is the same.
this.textBlock1.Text = "Some updates may not have been persisted";
}
catch (Exception ex)
{
// Other issues may cause errors - server availability, network
this.textBlock1.Text = "Unable to persist all updates. Error=\"" +
ex.Message + "\"";
}
// Refresh local collection
// Execute query again, getting fresh data from OData service,
// overwrite any lingering uncommitted changes in the DataGrid
ctx.MergeOption = MergeOption.OverwriteChanges;
members = new DataServiceCollection<MembersItem>(query);
this.dataGrid1.ItemsSource = members;
// Set Merge Options back to default
ctx.MergeOption = MergeOption.AppendOnly;
}
catch (Exception ex)
{
// Other issues may cause errors - server availability, network
this.textBlock1.Text = "Unable to persist all updates. Error=\"" +
ex.Message + "\"";
}
}
Another enumeration, SaveChangesOptions, can be used to tell the REST service whether to stop on the first error - that's the default, SaveChangesOptions.None - or to try all change operations in spite of errors, as in the code above, (SaveChangesOptions.ContinueOnError), or send the changes as a single batch, or use the PUT verb for updates. (MERGE is used by default.) If details of each change operation are needed, the DataServiceResponse object can be inspected. This is returned by the SaveChanges() method if all goes well, or found on the DataServiceRequestException exception if not. If it is needful to sort out which changes failed, any entity in the DataServiceResponse object with a state other than Unchanged denotes a failed operation.
To see what happens in these exchanges, it can be helpful to check things out in the QuickWatch window. Set a break point and drill down into the Data Context object like this:
Another way to see what's happening is Fiddler2, a free and wonderful tool for web debugging ( http://www.fiddler2.com/fiddler2/). In this screenshot, a successful update by the WPF app can be seen, with before (W/"12") and after (W/"13") ETags visible. When an update operations succeeds, the HTTP response is 204 No Content.
Happy RESTful coding! |
Dec
20
Published: December 20, 2011 05:12 AM by
Nancy Brown
Sandboxed Solutions are a very cool, very new feature in SharePoint 2010, a version 1.0 feature. Translation: watch out for bugs.
When a List Item Event Receiver is added to a SharePoint Project in a Sandboxed Solution, the associated Feature is automatically Web-scoped, and works as expected. But if the Feature scope is changed to Site, weird stuff happens. Let's consider the ItemDeleting event, which has extra weirdness.
The Visual Studio Event Receiver wizard has an Event Source dropdown for List Item Events:
For a List Item Event, that event source is typically a type of List, which is identified by a ListTemplateId attribute in the Receivers element:
In this example, the code will simply cancel the ItemDeleting event, and direct the business user to expire the Announcement instead. That code might look like this:
public override void ItemDeleting(SPItemEventProperties properties)
{
properties.Status = SPEventReceiverStatus.CancelWithError;
properties.ErrorMessage = "Please expire, rather than delete.";
}
As long as the Feature remains at Web scope, this works as advertised - Announcements cannot be deleted, and business users are presented with the message to expire rather than delete:

Try changing to a Site-scoped Feature, and deployment succeeds the first time. But the very next deployment fails, with Visual Studio muttering (in the Output window) that retraction failed, so Visual Studio just quits right there, and throws up its hands (figuratively speaking). Curiously, this does not happen with the ItemAdding or ItemUpdating events. Weirdly, if one attempts to deploy again, (third time's a charm), the "retraction" succeeds (!?!), and deployment succeeds. Here's what happened: during the failed deployment, the Sandboxed Solution is "retracted" (that is, deactivated in the Solutions Gallery), then Visual Studio chokes on deleting it. The next time, the Solution is already deactivated, so Visual Studio just attempts to delete it, succeeds in deleting it, then succeeds in deploying the new Solution.
Does this Site-scoped version work? Yep, a little too well. Try to delete an Announcement, and voila, the deletion is blocked, and the message appears. Switch to another type of List, a Tasks List. Try to delete a Task item, and that deletion is blocked, and the message appears. Try to delete an item from any type of List, and the deletion is blocked. Oops. The ItemDeleting Event Receiver now applies to every list, not just whatever was specified by the ListTemplateId attribute in the Receivers element in Elements.xml.
Remember that Event Source dropdown list in the Visual Studio Event Receiver wizard? Event Receivers are bound to Event Hosts, which are SharePoint objects: SPContentType, SPFile, SPList, SPListItem, SPWeb, SPSite, or SPWorkflow. (BTW," bound" is just another way to say "registered".) By poking around with PowerShell, one can see where an Event Receiver is bound. It turns out that when the Feature is Web-scoped, the Item Event Receiver is actually bound to individual Lists. When the Feature is Site-scoped, the Item Event Receiver is bound to the SPSite object. Here's the Web-scoped version as seen in the SharePoint 2010 Management Shell:
And here's the Site-scoped version:
So, a simple workaround for the Site-scoped version is to filter the Lists in the Event Receiver code, like this:
public override void ItemDeleting(SPItemEventProperties properties)
{
base.ItemDeleting(properties);
// At the Site Scope, the event receiver is registered with
// the Site Collection, not with individual Lists,
// so restrict it here
if (properties.List.BaseTemplate == SPListTemplateType.Announcements)
{
properties.Status = SPEventReceiverStatus.CancelWithError;
properties.ErrorMessage = "Please expire, rather than delete.";
}
}
To alleviate the annoying deployment issue, use a PowerShell script as a pre-deployment command to retract the Sandboxed Solution, leaving Visual Studio to just carry out the deletion. (Note: "Retract" is spelled "Uninstall" in PowerShell, hence the "Uninstall-SPUserSolution" below.) Here's the PreDeploy.ps1 PowerShell script:
Add-PSSnapin "Microsoft.SharePoint.PowerShell"
$wsp = "Mindsharp.ListItemEvent.SiteScope.wsp"
$url = "http://intranet"
Echo " Retracting Solution..."
Uninstall-SPUserSolution $wsp -Site $url -Confirm:$False
Pre-deployment commands are found on the SharePoint tab of the Project's properties. (Just double-click the Properties folder in Solution Explorer to see it.) Don’t forget to use the SysNative alias in the pre-deployment command, so that Visual Studio launches the right version of PowerShell:
%WINDIR%\SysNative\WindowsPowerShell\v1.0\powershell.exe -command "& '$(ProjectDir)PreDeploy.ps1'"

More Trouble in the Sandbox: Feature Receiver code in a Site-scoped Item Event Receiver Solution
What if this Sandboxed Solution also needs Feature Receiver code? Typically, a Feature Receiver would just be added to the existing Feature. If this were Web-scoped, that would work. If this were a Farm Solution, that would work. However, in a Sandboxed Solution, adding a Feature Receiver to the existing Site-scoped Item Event Receiver Feature can cause the WSP to get stuck in the Solutions Gallery - really, really stuck - can't delete it from the browser, can't delete using PowerShell. (Weirdly, it can be activated and deactivated.) Manual intervention can fix this.
To unstick the stuck WSP, save the Feature Receiver code somewhere, then delete the Feature Receiver. Build and package the solution (these options are on the Build menu). If the Solution's configuration is Debug, the newly packaged WSP will be in the Solution's bin/debug folder. Use the Show All Files button to see it in the Solution Explorer.
Browse to the Solutions Gallery, and upload the just-packaged WSP, (the one with no Feature Receiver code), overwriting the existing "stuck" WSP. Then deactivate and delete the WSP using the Ribbon.
With the stuck WSP out of the way, go back to Visual Studio, and add a new Site-scoped Feature, then add the Feature Receiver code to the new Feature. This should take care of the "stuck WSP" issue, and allow normal iterative development to keep flowing.
Hope that helps! |
Nov
08
Published: November 08, 2011 04:11 AM by
Nancy Brown
|
The new (2010) way to create custom site definitions is the Web Template, which is Sandbox-safe. These can be created using the Save-site-as-template on the Site Settings page, but that creates a humongous beast containing all the Fields, Content Types, Lists… way more stuff than is truly needed, so a better solution is the Do-It-Yourself (DIY) WebTemplate.
A DIY Sandboxed WebTemplate solution might look like this in Solution Explorer, with a Site Feature to deploy the WebTemplate to the Solutions Gallery, and another hidden Feature, (WebFeature in this case), to handle the custom provisioning of a new site.
This post is about just one aspect of this scenario: how to provision web parts to a page using a provisioning Feature in a Sandboxed WebTemplate Solution. The page in this example is default.aspx, a clone of the STS#1 (Blank Site) default.aspx, with some added text and an image. SiteFeature deploys the actual Web Template - that's everything in the FRED folder: ONet.xml and Elements.xml (which contains only a WebTemplate element). WebFeature deploys everything in the ProvisionModule folder: default.aspx and Elements.xml, which contains such bits as ListInstance elements, Modules, and PropertyBag elements. WebFeature is called by ONet.xml to help provision a new site.
List View Web Parts
Typically, the ONet.xml file for a Do-It-Yourself WebTemplate is created by selectively copying/pasting from the base site definition's ONet.xml. This could lead one to thinking "Oh, I’ll just grab the <List> elements too." Unfortunately, that won't work. Well, specifying the <List> element in ONet.xml will create the list, but if a <View> element is used in the provisioning Feature's xml to create the matching web part, that fails with this sort of error message in the log:
Failed to find a suitable list id for doc 'default.aspx' given List template attribute 'Lists/Announcements'.
Failed with 0x80004005 to create the view query or web parts for web /fred, site collection (unspecified), URL default.aspx
Solution - instead of using <List> in ONet.xml, use the new ListInstance element in the provisioning Feature (WebFeature in this example). Looks like this:
<?xml version="1.0" encoding="utf-8"?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<ListInstance FeatureId="{00bfea71-d1ce-42de-9c63-a44004ce0104}" TemplateType="104"
Title="Announcements"
Description=""
Url="Lists/Announcements"
OnQuickLaunch="TRUE"
RootWebOnly="FALSE" >
<Data>
<Rows>
<Row>
<Field Name="Title">Keep the Team Up-To-Date!</Field>
<Field Name="Body">
Use this List to post any SharePoint 2010 tips or tricks
</Field>
<Field Name="Expires">12/31/2011</Field>
</Row>
</Rows>
</Data>
</ListInstance>
<Module Name="ProvisionModule" Url="">
<File Path="ProvisionModule\default.aspx" Url="default.aspx" Type="Ghostable">
<View List="Lists/Announcements"
BaseViewID="1"
WebPartOrder="0" WebPartZoneID="Left" >
</View>
</File>
</Module>
</Elements>
Other web parts, with customizations
There are at least a couple of options here. Both involve customizing a web part through the browser, and then harvesting that customization somehow. Option one is exporting the customized web part and using that data in an AllUsersWebPart element. Option two is using the Save-site-as-template to create a WSP that is then imported into Visual Studio, followed by extracting the BinarySerializedWebPart element from it.
AllUsersWebPart
First customize a web part through the browser. The image web part is pretty straightforward, and we can use images in the {SharePointRoot} images folder, so let's use that. For option one, the AllUsersWebPart, we'll set the web part to show templogo.gif, left-justified, with no chrome and no title, then use the Export option.

Open the exported .dwp or .webpart file in Notepad, and copy the entire WebPart element. The opening WebPart tag only needs the xmlns=" http://schemas.microsoft.com…" attribute, so delete any others. Usually it is best also to delete the child elements that are default values as well. In the provisioning Feature's Elements.xml, add an AllUsersWebPart element with a CDATA section to contain the WebPart element just created from the export. (The AllUsersWebPart element can "host" almost every web part.) Note that the WebPartZoneID and WebPartOrder should be attributes of the AllUsersWebPart element, not child elements of the WebPart element. The AllUsersWebPart is added as a child element to the File element, just like the View element in the example above. The AllUsersWebPart should look something like this:
<AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
<![CDATA[
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
<Title />
<FrameType>None</FrameType>
<FrameState>Normal</FrameState>
<MissingAssembly>Cannot import this Web Part.</MissingAssembly>
<Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ImageWebPart</TypeName>
<ImageLink xmlns="http://schemas.microsoft.com/WebPart/v2/Image">/_layouts/images/templogo.gif</ImageLink>
<VerticalAlignment xmlns="http://schemas.microsoft.com/WebPart/v2/Image">Middle</VerticalAlignment>
<HorizontalAlignment xmlns="http://schemas.microsoft.com/WebPart/v2/Image">Left</HorizontalAlignment>
</WebPart>
]]>
</AllUsersWebPart>
BinarySerializedWebPart
Going back to the site where the image Web Part was customized earlier, let's add another web part, an XML Viewer, and use it to display the first five titles in a blog - that means adding some XSL. The XML Viewer Web Part is a bit different from most other web parts - one of its properties is an XSL transform. That means if one exports it, there is already a CDATA section in the XML. Here's an example with the embedded CDATA section highlighted:
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
<Title>XML Viewer</Title>
<FrameType>Default</FrameType>
<Description>Transforms XML data using XSL and shows the results.</Description>
<ZoneID>Left</ZoneID>
<PartOrder>0</PartOrder>
<FrameState>Normal</FrameState>
<MissingAssembly>Cannot import this Web Part.</MissingAssembly>
<Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.XmlWebPart</TypeName>
<XMLLink xmlns="http://schemas.microsoft.com/WebPart/v2/Xml">http://www.tinyurl.com/SPTeamBlog</XMLLink>
<XML xmlns="http://schemas.microsoft.com/WebPart/v2/Xml" />
<XSLLink xmlns="http://schemas.microsoft.com/WebPart/v2/Xml" />
<XSL xmlns="http://schemas.microsoft.com/WebPart/v2/Xml"><![CDATA[<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" indent="yes"/>
<xsl:param name="TITLE"/>
<xsl:template match="rss">
<div style="background:#ffffff; padding:0; font-size:10px;">
<xsl:for-each select="channel/item[position() < 6]">
<a href="{link}" target="_new"><xsl:value-of select="title"/></a><br/>
</xsl:for-each>
</div>
</xsl:template>
</xsl:stylesheet>]]></XSL>
<PartStorage xmlns="http://schemas.microsoft.com/WebPart/v2/Xml" />
</WebPart>
CDATA sections cannot be nested, so the AllUsersWebPart element is not much help here; but a BinarySerializedWebPart can be. Create a WSP for that site using the Save-site-as-template option on the Site Settings page. In the Solutions Gallery, click the name of the solution to download a copy to the hard drive. Fire up Visual Studio, and create a new Import SharePoint Solution Package project. Import the solution just downloaded, taking all the defaults. In Solution Explorer, expand the Modules folder, and find a folder that starts with "_SiteTemplates" - that's where the BinarySerializedWebPart will be. (In this example, the Site used for the web parts is a Blank Site - STS#1). Double-click Elements.xml to view it in the Visual Studio editor.

Copy the entire BinarySerializedWebPart element, and paste it inside the File element as before. Be sure to edit the Url attribute to match the deployment scenario, which in this case, would be just "default.aspx", and don't forget to update the WebPartOrder and WebPartZoneId attributes. The Module element with these three web parts now looks like this:
<Module Name="ProvisionModule" Url="">
<File Path="ProvisionModule\default.aspx" Url="default.aspx" Type="Ghostable">
<View List="Lists/Announcements"
BaseViewID="1"
WebPartOrder="0" WebPartZoneID="Left"
>
</View>
<AllUsersWebPart WebPartZoneID="Left" WebPartOrder="1">
<![CDATA[
<WebPart xmlns="http://schemas.microsoft.com/WebPart/v2">
<Title />
<FrameType>None</FrameType>
<FrameState>Normal</FrameState>
<MissingAssembly>Cannot import this Web Part.</MissingAssembly>
<Assembly>Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c</Assembly>
<TypeName>Microsoft.SharePoint.WebPartPages.ImageWebPart</TypeName>
<ImageLink xmlns="http://schemas.microsoft.com/WebPart/v2/Image">/_layouts/images/templogo.gif</ImageLink>
<AlternativeText xmlns="http://schemas.microsoft.com/WebPart/v2/Image">Temp Logo</AlternativeText>
<VerticalAlignment xmlns="http://schemas.microsoft.com/WebPart/v2/Image">Middle</VerticalAlignment>
<HorizontalAlignment xmlns="http://schemas.microsoft.com/WebPart/v2/Image">Left</HorizontalAlignment>
</WebPart>
]]>
</AllUsersWebPart>
<BinarySerializedWebPart>
<GUIDMap />
<WebPart ID="{d5d0e278-b7b8-4ca0-987a-08643cf85f50}"
WebPartIdProperty="" List="" Type="0" Flags=""
DisplayName="" Version="2"
Url="default.aspx"
WebPartOrder="2" WebPartZoneID="Left"
IsIncluded="True" FrameState="0"
WPTypeId="{1077a241-f086-1411-9623-a67ec78bc114}"
SolutionId="{00000000-0000-0000-0000-000000000000}"
Assembly="Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"
Class="Microsoft.SharePoint.WebPartPages.XmlWebPart"
Src=""
AllUsers="AQUAAAACKgBJACkAAz4AAAIqAEoAKQADPQAAAQIAKQAD//80VHJhbnNmb3JtcyBYTUwgZGF0YSB1c2luZyBYU0wgYW5kIHNob3dzIHRoZSByZXN1bHRzLg8BIAApAAP//xsvX2xheW91dHMvaW1hZ2VzL21zeG1sbC5naWYPAVEAXQAD///EAWh0dHA6Ly9zaGFyZXBvaW50Lm1pY3Jvc29mdC5jb20vYmxvZy9fbGF5b3V0cy9mZWVkLmFzcHg/eHNsPTImd2ViPSUyRmJsb2cmcGFnZT0yMjY1NDg5Ny01MzU1LTQwOWItODZiNy0xOTI5NTViYjU3ZDgmd3A9NjhjODA0NWYtMzcyZC00Zjg3LTkzNTMtNzk5MzZhY2ZhZDQ4JnBhZ2V1cmw9JTJGYmxvZyUyRlBhZ2VzJTJGZGVmYXVsdCUyRWFzcHgPARQAXQAE///sAzw/eG1sIHZlcnNpb249IjEuMCIgZW5jb2Rpbmc9IlVURi04Ij8+DQo8eHNsOnN0eWxlc2hlZXQgdmVyc2lvbj0iMS4wIiB4bWxuczp4c2w9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvWFNML1RyYW5zZm9ybSI+DQo8eHNsOm91dHB1dCBtZXRob2Q9Imh0bWwiIGluZGVudD0ieWVzIi8+DQo8eHNsOnBhcmFtIG5hbWU9IlRJVExFIi8+DQo8eHNsOnRlbXBsYXRlIG1hdGNoPSJyc3MiPg0KICA8ZGl2IHN0eWxlPSJiYWNrZ3JvdW5kOiNmZmZmZmY7IHBhZGRpbmc6MDsgZm9udC1zaXplOjEwcHg7Ij4NCiAgICAgIDx4c2w6Zm9yLWVhY2ggc2VsZWN0PSJjaGFubmVsL2l0ZW1bcG9zaXRpb24oKSAmbHQ7IDZdIj4NCiAgICAgICAgPGEgaHJlZj0ie2xpbmt9IiB0YXJnZXQ9Il9uZXciPjx4c2w6dmFsdWUtb2Ygc2VsZWN0PSJ0aXRsZSIvPjwvYT48YnIvPg0KICAgICAgPC94c2w6Zm9yLWVhY2g+DQogIDwvZGl2Pg0KPC94c2w6dGVtcGxhdGU+DQo8L3hzbDpzdHlsZXNoZWV0Pg8P"
PerUser="AQUAAAACKgBJACkAAz4AAAIqAEoAKQADPQAAAQQAKQAD//8UU2hhcmVQb2ludCBUZWFtIEJsb2cPDw==" />
</BinarySerializedWebPart>
</File>
</Module>
Here's a new site is created from that DIY WebTemplate, displaying the customized web parts on default.aspx.
Hope that helps! |
Oct
26
Published: October 26, 2011 20:10 PM by
Nancy Brown
If new to Visual Studio 2010, a few gotchas may get you when first starting to work with Do-It-Yourself (DIY) Sandboxed Web Templates, the preferred SharePoint 2010 way to create custom site definitions. (If using the Save-site-as-template version, this won't apply.)

Web Templates created by the Save-site-as-template option on the Site Settings page are kinda like html emitted by MS Word – very faithful to the original object, way more "markup" than optimal for those who are looking for simple and clean. Manually created Web Template solutions can be simple and clean, and have two possible deployment scopes: Farm and Site. This post focuses on creating a Web Template Solution from scratch and Site-level deployment, since Sandboxed Solutions are welcome in the cloud. When doing so, it is possible to trip in a number of spots. If fact, it can feel like a situation comedy at times, as we follow the plight of a star-crossed default.aspx when it deploys (or doesn't), or deploys too many times, or gets deleted by accident.
But first off, what can be learned from a Save-site-as-template version? Create one, import it to Visual Studio, and discover that the ONet.xml file consists basically of the NavBars element, and one Configuration element with its SiteFeatures and WebFeatures child elements. A simple and clean way to create a custom ONet.xml is to copy & paste portions from the base site definition's ONet.xml, and imitate the Save-site-as-template variety by copying just the NavBars element, and one Configuration element, with its Site and Web Features only. (It's actually a requirement - Web Template ONet.xml files may only have one configuration element.)
If the Configuration element contains Module elements, probably delete them, and use a Feature instead. (Features can be versioned and upgraded.) Also, beware - not all "Module"s are the same. There's Module Element (Site) – the one in ONet.xml - and then there's Module Element (Module) – the one used in regular old Feature xml. Looking up Module Element (Site) on MSDN reveals that the Path attribute is "Optional Text. Specifies the physical path to the file set relative to %ProgramFiles%\Common Files\Microsoft Shared\web server extensions\14\TEMPLATE\SiteTemplates\Site_Definition". OK, so that's just for grabbing the original core site definition files. Trying to bend this Module element into deploying a custom file in the solution, (like default.aspx), will result in the first Situation Comedy episode (SitCom # 0) - the original file deploys, not the custom one.
A simple custom (DIY) Sandboxed Web Template Solution needs just two Features and three basic files:
- a Site scoped Feature to deploy the WebTemplate to the Solutions Gallery, containing
- an Elements.xml for the WebTemplate element, which says something like "Hi, my name is 'Fred' and I'm based on the 'BlahBlah' core site definition"
- an ONet.xml file in the same Visual Studio folder, which contains provisioning details
- a Web scoped Feature for custom provisioning
- this provisioning Feature will be listed in ONet.xml; its Elements.xml file handles most of the customizations that make this site different from the base site

Alrighty then, that Web scoped Feature needs to be Hidden and should not be activated by Visual Studio on deployment (the default), so in the Feature properties, set IsHidden to True, and Activate on Default to False…deploy…and what the heck?!? The Web Feature auto-activated anyway, accidentally customizing the Project's test site, Intranet, with a new default.aspx. Turns out the Activate on Default property does not apply to Site and Web scoped Features. Bummer. So, go to the Project's properties, find the SharePoint tab, and set the Active Deployment Configuration to No Activation. Use a PowerShell post-deployment script to activate the Site Feature, or manually activate it. Open the site in SharePoint Designer to fix default.aspx. And save a copy - just in case.

OK, that's fixed up, try again…deploy. Browse over to the Intranet site, just to be sure that WebFeature didn't sneak by somehow… and Gaaaaah! Intranet displays a 404 Not Found. What happened? Look in the Output window, where Visual Studio narrates what it's doing. Hmmm… Found 1 deployment conflict(s) … deleting http://intranet/default.aspx .... not good. Inspect the Module's properties in the properties window. Aha. Deployment Conflict Resolution is set to Automatic. Change that to None. Use SharePoint Designer [again] to fix the missing default.aspx. Good thing there was a backup copy of default.aspx.

Last gotcha: things that must match.
The WebTemplate's BaseConfigurationID and Onet.xml's Configuration Id must match. If they don't, Visual Studio will complain:
"Error occurred in deployment step 'Add Solution': …The Mindsharp.WebTemplates.Research_SiteFeature\FRED\onet.xml file must contain a Configuration element whose ID attribute is equal to the BaseConfigurationID attribute declared in the WebTemplate element."

Finally, the WebTemplate element's Name attribute and the Folder name which contains the Elements.xml file must be identical - in this case, it's FRED.

Have fun creating DIY Web Templates!
Sep
02
Published: September 02, 2011 15:09 PM by
Nancy Brown
In SharePoint 2010 Wiki pages, missing-web-part errors can be hard to see - (now there's an understatement!) On a Wiki page, the offending web part simply disappears. On a Web Part page, the error is visible.
Here's a demo web part on a Wiki page (it's using the gears_an.gif in the images directory):
And here it is on a Web Part Page:
An unhandled exception in a web part will result in the usual Server Error on either kind of page. But something very different happens when the web part is inaccessible for some reason, say, a missing Safe Control entry, or the version number has changed.
By default, the assembly version in a new SharePoint project is 1.0.0.0. For this demo, we'll change the assembly version number to 2.0.0.0, and redeploy the CanYouSeeMeNow web part. The metadata on the pages where the web part is already deployed will still be pointing to version 1.0.0.0, so the web part will go missing.
Here is the error on the Web Part page, where it is clear that this is the "CanYouSeeMeNow" web part:
But there's nothing on the Wiki Page, just an empty white area:
Well, it is certainly clear that a web part is causing an issue, but completely unclear which web part that might be - it is now just "ErrorWebPart". (This is the case for the Web Part page's maintenance page as well.)
Putting the Wiki page in edit mode does not help - the web part is still invisible. But there is a way.
Browse back to the Wiki page, where the "error" web part is invisible. First, get an inventory of what's in the Closed Web Parts gallery. Put the page in edit mode, and choose Editing Tools > Insert > Web Part on the Ribbon. Under Categories, click the Closed Web Parts link. Note how many closed web parts there are, and their titles, so that you'll be able to find the "ErrorWebPart" after it is closed. (Hopefully, there will be no closed web parts, as it is generally better to delete them rather than close them. A bunch of closed web parts can make the page mysteriously slow to load.)
Return to the Web Part Maintenance page, and close that ErrorWebPart.
Now browse back to the Wiki page. Put the page in edit mode again, and visit the Closed Web Parts gallery. Locate the ErrorWebPart (in this demo, it showed up as "Untitled").
Click the Add button to restore it, and voila, there's the error report and web part name. Of course, after exiting edit mode, the web part disappears again.
Hope that helps!
Jul
26
Published: July 26, 2011 14:07 PM by
Nancy Brown
LINQ to SharePoint is projected to save many developers from ever having to write CAML again - and it just might do it! Like many LINQ providers, entity classes are required, and typically are created by a custom tool, SPMetal in this case, found in the {SharePointRoot}\bin folder. The entity classes created by the SPMetal tool only cover built-in List Fields (Columns). To get a List Item's properties (like File, or Attachments), or properties in the List Item's Property bag, or custom Field Types, more is needed - an extension of the generated entity classes. And there is a way to do just that, covered in some detail on MSDN - see Extending the Object-Relational Mapping. To recap that article very briefly, these are the salient tasks to extend an SPMetal-generated Content Type entity class:
- Create a new class file containing a public partial class with the same name as the SPMetal-generated class that represents the target Content Type; this class must implement the ICustomMapping interface. This is the extended class.
- Create properties in the extended class to represent the target properties of the List Item
- Create code in the MapFrom method (fetches FROM the Content database) and MapTo method (writes TO the content database) that manages the relationship between the extended class properties and the actual List Item's properties
- Add concurrency managing code to the Resolve method, per MSDN's instructions
MapFrom and MapTo methods are the couriers, transferring data from the List Item proper and the matching properties in the extended class. For most List Item properties this works well. The MapTo method generally just assigns the extended class's property to the actual List Item's property. For example, to deal with a List Item's property bag's entry for cell phone, the extended class would have a custom CellPhone property to represent it, and the MapTo method would assign that CellPhone property to the actual List Item's property bag. It might look like this:
string cellPhoneKey = "CellPhone";
// CellPhone property in Property Bag
private string cellPhone;
public string CellPhone
{
get
{
return this.cellPhone;
}
set
{
if ((value != this.cellPhone))
{
this.OnPropertyChanging(cellPhoneKey, this.cellPhone);
this.cellPhone = value;
this.OnPropertyChanged(cellPhoneKey);
}
}
}
[CustomMapping(Columns = new String[] { "*" })]
public void MapFrom(object listItem)
{
SPListItem item = (SPListItem)listItem;
if (item.Properties.ContainsKey(cellPhoneKey))
{
this.CellPhone = item.Properties[cellPhoneKey].ToString();
}
else
{
// first time
item.Properties.Add(cellPhoneKey, string.Empty);
}
}
public void MapTo(object listItem)
{
SPListItem item = (SPListItem)listItem;
// CellPhone Property in Property Bag
if (!item.Properties.ContainsKey(cellPhoneKey))
{
// First time
item.Properties.Add(cellPhoneKey, this.CellPhone);
}
else
{
item.Properties[cellPhoneKey] = this.CellPhone;
}
}
// Resolve method omitted
But what happens with Attachments? It is a read-only property, so assigning to the List Item in the MapTo method is not possible. No code in MapTo means calling SubmitChanges() on the data context object will not update the extended property for Attachments. So here is a workaround. Attachments can still be fetched and updated, but without the concurrency management. The code in the extended class might look like this:
// Attachments
private SPAttachmentCollection attachmentCollection;
public SPAttachmentCollection AttachmentCollection
{
get
{
return this.attachmentCollection;
}
set
{
if ((value != this.attachmentCollection))
{
this.OnPropertyChanging("AttachmentCollection", this.attachmentCollection);
this.attachmentCollection = value;
this.OnPropertyChanged("AttachmentCollection");
}
}
}
// Set property values FROM the content database
[CustomMapping(Columns = new String[] { "*" })]
public void MapFrom(object listItem)
{
SPListItem item = (SPListItem)listItem;
this.AttachmentCollection = item.Attachments;
}
// Save a value TO the content database
public void MapTo(object listItem)
{
SPListItem item = (SPListItem)listItem;
// SPAttachmentCollection is a read-only property,
// so nothing here
}
Fortunately, updates can be handled directly in the client code. SPAttachmentCollection has AddNow, DeleteNow, and RecycleNow methods, which do not require an SPItem.Update() call. Using this as a workaround, the client code to add to Attachments might look something like this:
protected void addAttachment(Object sender, EventArgs e)
{
if (string.IsNullOrEmpty(this.newFile.PostedFile.FileName))
{
return;
}
ProjectMembersItem item = GetSelectedProjectMembersItem();
Stream fs = this.newFile.PostedFile.InputStream;
byte[] fileBytes = new byte[fs.Length];
fs.Read(fileBytes, 0, (int)fs.Length);
fs.Close();
// Get file name from file path
string fileName = this.newFile.PostedFile.FileName
.Split(new string[] { "\\" }, StringSplitOptions.None)
.LastOrDefault();
try
{
// No call to SubmitChange(),
// because this updates immediately
item.AttachmentCollection.AddNow(fileName, fileBytes);
}
catch { }
}
In this example, this.newFile is an HtmlInputFile control (allowing the user to browse to a file to upload) and method GetSelectedProjectMembersItem fetches the previously user-selected List Item, which will receive the new attachment. ProjectMembersItem is a Content Type class generated by SPMetal, so there is no Attachments property there. The item.AttachmentCollection property comes from the custom class extension. Attachments are updated immediately by the AddNow method, so no need for SubmitChanges (but no concurrency checks either)… nevertheless, mission accomplished.
Happy coding!

Jun
29
Published: June 29, 2011 06:06 AM by
Nancy Brown
|
SharePoint has its share of puzzles - like SPWeb.AllProperties and SPWeb.Properties - two property collections?? Why? Evolution - SharePoint is growing, changing, and leaving behind old ways (SPWeb.Properties) for better ways (SPWeb.AllProperties). The old way, SPWeb.Properties, is a property bag, a string dictionary, which contains only a subset of metadata for the web. As a string dictionary, it uses case-insensitive keys (all keys are lowercased). SPWeb.AllProperties, a Hashtable, contains all properties (metadata) for the web, and not only uses case-sensitive string keys, it can take other objects as keys, like DateTime or Int. But there's more puzzlement.
Sandboxed solutions are new, and like any new software, there are discoveries to be made and issues to be worked out. For those familiar with storing info in the SPWeb.AllProperties Hashtable, using the Hashtable methods like SPWeb.AllProperties.Add() or SPWeb.AllProperties.Remove() seems like the thing to do. But it isn't in a sandboxed solution, where it will fail silently (and perplexingly)! |
|
|
In SharePoint 2010, four new methods were introduced to work with the AllProperties collection:
- AddProperty
- DeleteProperty
- GetProperty
- SetProperty
In a sandboxed solution, these must be used, (not the Hashtable methods), and followed by SPWeb.Update() to persist any changes to the database:
if (!web.AllProperties.ContainsKey("MindsharpTest"))
{
web.AddProperty("MindsharpTest", "2010");
web.Update();
}
Hope that helps!
|
Jun
20
Published: June 20, 2011 11:06 AM by
Nancy Brown
Sometimes there just aren't enough choices on the Create Column page. When that happens, a custom Field Type might be the answer. Custom Fields Types, like many extensibility points in SharePoint, have a dizzying array of options, and often it is helpful to have a bird's eye view of the basics before digging into the MSDN documentation. So here goes.
Custom Field Types show up after the built-in Field Types (Column Types) on the Create Column or Change Column pages:
Field Type Definition Files
Custom Field Types are registered with SharePoint by a custom Field Type Definition file in the {SharePointRoot}\TEMPLATE\XML folder. The custom Field Type Definition file must follow a naming convention of fldtypes_[CustomFieldTypeName].xml, and contain only prescribed elements, primarily <Field Element (Field Types)> elements. SharePoint's own Field Type Definition file, FLDTYPES.XML, is in the same folder, and can be a valuable resource in finding parent types and default settings when the custom Field Type extends an existing one (which is the usual case). When the custom Field Type Definition file is deployed (or redeployed), an IISRESET is necessary to nudge SharePoint to pick up the new file.

In SharePoint 2010 custom Field Type Definition files, the old RenderPattern and PropertySchema elements are obsolete. Well, sort-of. If there are variable properties in the custom Field Type, the PropertySchema element is still necessary - just set the HIDDEN attribute to true, to turn off the default control rendering. A custom Field Type's "variable properties" typically show up in the "Additional Column Settings" portion of the Create / Change Column pages. These are custom properties of the custom Field Type that the business user may set to customize the Field/Column.

The 2010 methodology for creating a Field's property editor control for a Field's variable property (like the dropdown list in the image above), is a user control deployed to the {SharePointRoot}\TEMPLATE\CONTROLTEMPLATES folder - or a subfolder inside CONTROLTEMPLATES. The code-beside of the user control must implement the IFieldEditor interface. The custom Field Type Definition file, (fldtypes_FlashField.xml in this example), registers this property editor control through a Field element with a Name attribute of "FieldEditorUserControl" and a value that is the server relative url to the .ascx file.
Persisting Custom Properties of Custom Field Types
Custom Field Components
The custom Field typically consists of two or three classes (one is optional), a user control to handle the New, Edit, and View dialogs' rendering of the string data stored in the database, and an optional XSL file to handle any custom rendering in the List View. Note that the data persisted to the Content database, and displayed in the New and Edit dialogs is string data, but what is rendered on the View dialog and the List View may be more than that. Here's a composite image of the three List dialogs, with an example of the HTML from the user control visible in the View dialog:
Two classes partner together to handle most of the work: a Field class and a Field Control class. The Field class must be derived from SPField, or one of these classes which extend SPField:
- SPField
- SPFieldBoolean
- SPFieldChoice
- SPFieldCurrency
- SPFieldDateTime
- SPFieldLookup
- SPFieldMultiChoice
- SPFieldMultiColumn
- SPFieldMultiLineText
- SPFieldNumber
- SPFieldRatingScale
- SPFieldText
- SPFieldUrl
- SPFieldUser
According to MSDN, although there are many other classes that extend SPField, only the above are acceptable as the basis of a custom Field. The Field class typically handles the validation of any user entered data (although there may be validation in the Field Control class), and is command central. It is linked to its partner class though overridden public property FieldRenderingControl, which instantiates the Field Control class.
The Field Control class must extend Microsoft.SharePoint.WebControls.BaseFieldControl and handles setting the associated user control's HTML elements for the New, Edit, and View dialogs, usually according to which SPControlMode (Display, Edit, New) is in play in BaseFieldControl.ControlMode. That associated user control is deployed to the {SharePointRoot}\TEMPLATE\CONTROLTEMPLATES folder, like the editor control earlier, but unlike the editor control, it must be deployed directly to the CONTROLTEMPLATES folders (not a subfolder), or SharePoint won't find it. The user control is typically very minimal, just a set of RenderingTemplates whose IDs are referenced in any of several *TemplateName properties of the Field Control Class. There are several alternative TemplateName and Template properties for cases where a Field needs special rendering for special circumstances. In the Field Control class, there are two value properties: Value - which is the value in the user control UI (that is, the New, Edit, and View dialogs), and ItemFieldValue, which is the saved value in the partner Field class (the serialized string from the database).
If the Field data is complex, then the third, optional class may be needed: a custom Field Value class. (This could also be a struct.) There is no requirement that this derive from any particular class, but it may be helpful to use an existing one, such as SPFieldMultiColumnValue. A custom Field Value class must implement two constructors, one with no parameters and one with a String parameter. If they don't inherit from a built-in class, most Field Value classes should be marked Serializable and implement the ToString method. This class functions as a helper class, hiding data complexity, and simplifying data access in the other classes.

XSLT List View Rendering
XSLT List View rendering is new in SharePoint 2010. Previously in SharePoint, List View rendering was handled by the CAML RenderPattern elements of the Field Type Definition file. Now it is handled by XSL in the {SharePointRoot}\TEMPLATE\LAYOUTS\XSL folder - which is a good bit more flexible. The two most important of these XSL files are vwsytles.xsl and fldtypes.xsl. Often the default XSL transformation will be sufficient for a custom Field Type that inherits from a built-in derived type. If not, then a custom XSL can be deployed to the {SharePointRoot}\TEMPLATE\LAYOUTS\XSL folder. This custom XSL must follow a naming convention (much like the Field Type Definition file earlier): fldtypes_[CustomFieldTypeName].xsl. Also, the custom XSL should be modeled after the default XSL for the base type, but should alter the match attribute so that only the custom field gets the custom XSL.
For legacy custom Field Types, to retain the CAML rendering provided by the RenderPattern elements of the Field Type Definition file, implement (in the Field Type Definition file):
<Field Name="AllowBaseTypeRendering">TRUE</Field> <Field Name="CAMLRendering">TRUE</Field>
There is quite a bit to learn about the new XSLT rendering scheme, and the following articles are a good starting place for a custom XSL:
Overview of XSLT List View Rendering System
How to: Customize the Rendering of a Field on a List View
Examples of Input and Result Node Trees in XSLT Transformations
Essentially, a custom XSL will run on List data after basic transformations have taken place, and is responsible for the final polishing of the List View display. In the case of the example custom Field Type we've been following in this article, FlashField, that means custom XSL creating the standard object and embed tags for a Flash (SWF) video, and which inserts the user-entered url, video title, and video size into the object and embed elements. If XSL is new to you, expect to invest some time learning it; it is a declarative language which incorporates some functional language ideas.
Recall that video url and (optional) video title are List Item assets, and video size is a property of the Field/Column. The following List View screenshot shows the results of the custom XSL in two instances of the FlashField column, one configured for small video size, one configured for medium video size:

That's all for the bird's eye overview. Hope this helps!
May
18
Published: May 18, 2011 07:05 AM by
Nancy Brown
A fairly frequent occurrence for any developer is knowing what to do, but not how to say it in a new technology. Learning some WPF data binding as part of creating a SharePoint REST demo took a bit longer than anticipated, so here's hoping this post will save someone else that time. The goal: data binding a WPF DataGridComboBoxColumn in code, for a SharePoint Lookup field returned from a REST query, with the intention of performing CRUD operations from the WPF application. The final demo WPF application looks like this:

Lookup columns in SharePoint work like foreign key relationships in database tables. In this example, List ProjectMembers has Lookup Column "Role" which looks up column "ProjectRole" in List ProjectRole. The connecting "key" is the built-in ID column of the ProjectRole List. Lookup columns (AKA Lookup Fields) keep track of the related List in the SPFieldLookup.LookupList property, and the related field in the SPFieldLookup.LookupField property. Then, what is actually stored as data in the Lookup column is the ID (which acts like a foreign key) and the display value, delimited by the two characters: ";#". In this example, if 2 is the ID for ProjectRole List Item "Technical Lead", then when the ProjectMembers Lookup column "Role" displays "Technical Lead", the actual data stored in the Role column is 2;#Technical Lead
REST in SharePoint 2010 is the OData protocol for SharePoint Lists. Every SharePoint 2010 List has a built-in OData service at {SiteRoot}/_vti_bin/ListData.svc.
In the REST query results, the actual data returned for a Lookup column is a bit different than what is stored in the column; it is only the ID of the related Lookup List Item. The Lookup ID value is stored in an element whose name is constructed by catenating the Lookup column name and "Id" - so in this example, that would be "RoleId". This can be seen in a browser:
Like the SPFieldLookup class stores the related List and Field information in its properties, SharePoint REST (OData) stores the relationship information in its AssociationSets. Great! When using REST, the only thing needed for CRUD operations on a SharePoint Lookup Column is just that ID value.
For this WPF-SharePoint REST demo project, the REST query results were stuffed in a plain old DataTable, and the default view of the DataTable bound to the WPF DataGrid's ItemsSource property. The DataTable was used as a simple way to get the full editing capabilities of the WPF DataGrid, and to autogenerate columns. For the early part of the demo, having autogenerated columns meant that neither XAML markup nor the code needed any special data binding, and the query could be changed at will. But for CRUD operations involving Lookup columns, more was needed. Can't have business users guessing what RoleId "2" means - they need a dropdown list of text values. So, the autogenerated column for the Lookup field needed replacing with something that had a ComboBox, and that replacement column would need databinding.
The WPF DataGrid AutoGeneratingColumn event handler looked like a good place to swap columns. So I swapped the generated column for a DataGridComboBoxColumn. So far, so good. How does this bind in code? This wasn't complicated in Windows Forms… But as a newbie to WPF, this had me baffled for a bit, and most information found online demonstrated binding in XAML markup, not code. Was an ObjectDataProvider needed? What kind of binding is needed, and which properties should be bound? TextBinding? SelectedItemBinding? SelectedValueBinding? SelectedValuePath?
Clearly the ComboBox will need a collection of its own. The ProjectRoleItem collection that is part of the OData Entity Data Model would be fine for this. Inspecting a ProjectRoleItem object in the browser reveals the property names needed are "Id" and "ProjectRole":
Just one line of code is needed to create that collection as a List (ctx is the DataContext object):
List<ProjectRoleItem> roleList = ctx.ProjectRole.ToList();
A DataGridComboBoxColumn has two parts: a Column and a ComboBox. It appears that often in WPF data binding, a parent object is bound to a collection, and child objects need to specify just the PropertyPath in that collection. Since the DataGrid is already bound to the DataTable's default view, the column part of the DataGridComboBoxColumn just needs binding to the DataTable column name. Assigning a new Binding("RoleId") to the SelectedValue property takes care of the column portion of the DataGridComboBoxColumn.
The ComboBox's ItemsSource can bind to the collection of ProjectRoleItems, and then must be informed what in the collection is the key, and what is the display value. With that established, the column portion's value acts like a foreign key to the ComboBox's ItemsSource.
Putting it all together, the final AutoGeneratingColumn event handler code looks like this:
private void dataGrid1_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
{
if (e.PropertyName == "RoleId")
{
DataGridComboBoxColumn column = new DataGridComboBoxColumn();
// DataGrid is already bound to DataTable;
// so bind this new DataGrid column to a DataTable column-
column.SelectedValueBinding = new Binding("RoleId");
// Now bind the ComboBox part
column.ItemsSource = roleList;
column.DisplayMemberPath = "ProjectRole";
column.SelectedValuePath = "Id";
// Give it a column heading
column.Header = "Role";
// Swap the columns
e.Column = column;
}
}
Hope that helps. Happy RESTing.
Apr
05
Published: April 05, 2011 05:04 AM by
Nancy Brown
In part 3, we will investigate how hierarchical Claims are surfaced in the Claims Picker so they can be discovered and used for authorization.
Picker Entity
The entity that a Claims Picker resolves, finds, and returns is a PickerEntity. It's what shows up on the right pane of the Select People and Groups -- Webpage Dialog. It's also what shows up in the tooltip that contains partial matches after running Check Names. It's that thing that is underlined and followed by a semi-colon after it is validated (resolved) in the Claims Picker. A PickerEntity, (a .Net class), can represent many things: a custom Claim, a Windows user, an FBA user, an FBA role, an Active Directory Domain Group - that is, some security principal that can be used for authorization. A PickerEntity is the vehicle to surface a Claim; it has a Claim property (an SPClaim object), and a DisplayText property (the Claim Value), among others. PickerEntitys are circled in the image below.
Tree Nodes
The tree on the left pane of the Select People and Groups -- Webpage Dialog needs a little explanation. The tree as visualized in the user interface (UI) is just what would be expected- here's a quick text sketch of the beginning of the tree, for reference:
Country USA USA-North Minnesota … USA-East Maine … USA-South Virginia … USA-West Alaska …
Each node that is not the root or a leaf is both a parent and a child node. The parent-child node relationships are evident in the UI. In the code, it can be a little confusing at first, because there can be two kinds of "children" for each tree node. For example, the USA node is the parent of all USA region nodes: USA-North, USA-East, USA-South, USA-West. These are contained in the USA node's Children property, a SPProviderHierarchyNode[]. For search results, that same USA node may also have PickerEntity children, (a List<PickerEntity>), in the node's EntityData property. In Search results, PickerEntities show up in the right pane of the Select People and Groups -- Webpage Dialog, with a Group heading label that matches the parent node in the left pane.
FillResolve
Let's look at method FillResolve, since it appears first in the UI. It has two overloads. The first overload is fired when the user clicks the Check Names link, and the code would look something like this:
XDocument Geo = default(XDocument);
internal const string Unknown = "unknown";
protected override void FillResolve(Uri context, string[] entityTypes,
string resolveInput, List<PickerEntity> resolved)
{
// proceed only if looking for correct entity type
if (!entityTypes.Contains(SPClaimEntityTypes.FormsRole))
return;
// Search all possible Claims values for a match or partial match with the
// input parameter string resolveInput. For each match/partial match found,
// add an entry to the parameter List<PickerEntity> resolved.
// The Claims Picker will detect whether any of the List<PickerEntity>
// entries are exact matches, and indicate a match (resolution) by underlining
// the string and adding a trailing semi-colon. For non-matches, a squiggly
// underline is created, and a tooltip alerts the user to partial matches.
string inputText = resolveInput.ToLower();
string groupName = default(string);
EnsureGeoData(); // This loads the geographic data XML file into the XDocument "Geo"
string c = Unknown;
string r = Unknown;
string p = Unknown;
foreach (var country in Geo.Descendants("Country"))
{
try { c = country.Attribute("name").Value; }
catch { }
if (c != Unknown && c.ToLower().Contains(inputText))
{
groupName = GetHierarchyNodeNameForNodeID(CountryClaimType);
PickerEntity pickerEntity = CreatePickerEntity(
CountryClaimType, c,
CountryClaimValueType, groupName);
resolved.Add(pickerEntity);
}
// regions
foreach (var region in country.Descendants("Region"))
{
// remaining code omitted for brevity . . .
}
This searches the GeoRegion Claims datasource, (an XML file), for all possible matches to the text the user typed into the Users/Groups textbox. All matches (whether exact or partial), are added as PickerEntities to the parameter List<PickerEntity> resolved. The Claims Picker will determine whether any are exact matches.
This image shows the squiggly underlining indicating a non-match.

When the user clicks on the squiggly-underlined text, a tooltip shows the partial matches returned by the Claims Provider:

The helper methods GetHierarchyNodeNameForNodeID and CreatePickerEntity need some explanation. They will be discussed after looking at the second overload of method FillResolve.
The second overload differs in that it is called by SharePoint implicitly, after a successful search on the Claims Picker search page, before it returns to the Claims Picker control. This is simpler, since the Claim value is already known, and just needs to be reported as a PickerEntity. That code would look something like this:
protected override void FillResolve(Uri context,
string[] entityTypes, SPClaim resolveInput,
List<PickerEntity> resolved)
{
// proceed only if looking for correct entity type
if (!entityTypes.Contains(SPClaimEntityTypes.FormsRole))
return;
string groupName = GetHierarchyNodeNameForNodeID(resolveInput.ClaimType);
PickerEntity pickerEntity =
CreatePickerEntity(resolveInput.ClaimType,
resolveInput.Value, resolveInput.ValueType, groupName);
resolved.Add(pickerEntity);
}
Here it is in action. In this image, a search for "vir" returned two partial matches, West Virginia and Virginia:

If the user selects one of these, and clicks the Add button, the second FillResolve method overload fires (the one with SPClaim as the third parameter). Subsequently clicking the OK button will close this dialog and return to the Claims Picker control.
Claim Types are HierarchyNodeIds
Key to understanding this hierarchy implementation is the use of Claim Types as node IDs, and that left pane (UI) tree node labels and right pane search result (UI) group labels are the same. Claim Types have been constructed to be self-describing.
In both methods FillHierarchy and FillSearch, a parameter of type SPProviderHierarchyTree is passed in. This represents the tree visible in the left pane of Select People and Groups -- Webpage Dialog. Each node of the tree is internally labeled with an ID (hierarchyNodeID), and that ID is the Claim Type. Tree nodes are type SPProviderHierarchyNode; the constructor looks like this:
New SPProviderHierarchyNode(string providerName, string nodeName, string hierarchyNodeID, bool isLeaf);
The second parameter, nodeName, is what appears as the UI node label in the left pane of the Webpage Dialog, while hierarchyNodeID is the internal label. When search results appear in the right pane, nodeName will become the group name. Let's see it.

Search results for USA regions where the region name contains "th":

The USA node above is a USA region Claim Type. Any Claim Values found by the search are stored inside the USA region node, in property EntityData.
A couple of base class methods should be used when creating SPClaims or PickerEntities; not using them can lead to errors. (See Tips and Known Issues - http://msdn.microsoft.com/en-us/library/ff625762.aspx - for more information.) Here's an example of creating PickerEntities and Claims using base class methods CreatePickerEntity and CreateClaim:
private PickerEntity CreatePickerEntity(string claimType,
string claimValue, string claimValueType, string groupName)
{
PickerEntity entity = base.CreatePickerEntity();
entity.Claim = CreateClaim(claimType, claimValue,
claimValueType);
entity.EntityType = SPClaimEntityTypes.FormsRole;
entity.EntityGroupName = groupName;
entity.DisplayText = claimValue;
entity.Description = groupName + ": " + claimValue;
entity.IsResolved = true;
return entity;
}
Helper method GetHierarchyNodeNameForNodeId method just decodes the Claim Type passed to it, and returns the embedded value closest to the leaf level:
private string GetHierarchyNodeNameForNodeID(
string hierarchyNodeId)
{
if (string.Equals(hierarchyNodeId,
CountryClaimType,
StringComparison.OrdinalIgnoreCase))
{ return "Country"; }
Hashtable claimBits = DecodeClaimType(hierarchyNodeId);
if (Unknown != (string)claimBits["Region"])
return (string)claimBits["Region"];
else
return (string)claimBits["Country"];
}
Example: For Claim Type "http://sharepointdevelopers.net/2011/02/claims/Country#Canada/Region", GetHierarchyNodeNameForNodeId would return Node Name "Canada".
FillHierarchy and FillSearch
These two methods interact with each other, and so are discussed together. Method FillHierarchy alters the left pane of the search page, and sets it up to filter the search. It is called when the user clicks on a hierarchical level, and adds the next child level nodes. For example, clicking the Country node adds all its possible child nodes:

Clicking the USA node adds all its possible child nodes:

If the user clicks a node, thus selecting it, and then searches, any search will be scoped to just the level that node represents like this:

The code would look something like this:
protected override void FillHierarchy(Uri context,
string[] entityTypes, string hierarchyNodeID,
int numberOfLevels, SPProviderHierarchyTree hierarchy)
{
// proceed only if looking for correct entity type
if (!entityTypes.Contains(SPClaimEntityTypes.FormsRole))
return;
EnsureGeoData();
// A null id indicates the root of the tree
// Add hierarchy root node
if (null == hierarchyNodeID)
{
hierarchy.AddChild(
CreateHierarchyNodeForNodeID(CountryClaimType,
false));
}
// Add Region nodes (scopes search to region)
if (IsACountryClaimType(hierarchyNodeID))
{
foreach (var country in Geo.Descendants("Country"))
{
string c = Unknown;
try { c = country.Attribute("name").Value; }
catch { }
if (c != Unknown)
{
string claimType = GetRegionClaimType(c);
hierarchy.AddChild(
CreateHierarchyNodeForNodeID(claimType, false));
}
}
}
// Add Province nodes (scopes search to province)
if (IsARegionClaimType(hierarchyNodeID))
{
Hashtable claimBits = DecodeClaimType(hierarchyNodeID);
string c = (string)claimBits["Country"];
try
{
var regions = Geo.Descendants("Country")
.Where(country =>
country.Attribute("name").Value == c)
.FirstOrDefault().Descendants("Region");
foreach (var region in regions)
{
string r = region.Attribute("name").Value;
string claimType = GetProvinceClaimType(c, r);
hierarchy.AddChild(CreateHierarchyNodeForNodeID(
claimType, true));
}
}
catch { }
}
}
// Helper method
private SPProviderHierarchyNode
CreateHierarchyNodeForNodeID(string hierarchyNodeID,
bool isLeaf)
{
return new SPProviderHierarchyNode(this.Name,
GetHierarchyNodeNameForNodeID(hierarchyNodeID),
hierarchyNodeID, true);
}
The FillHierarchy code decodes the Claim Type passed in as parameter hierarchyNodeID, and figures out what part of the hierarchy was clicked. Using the decoded Claim Type, it looks up the same node in the GeoRegion datasource, finds the child nodes there, then uses those to create new SPProviderHierarchyNode nodes, and adds them to parameter SPProviderHierarchyTree hierarchy, which is the node the user clicked.
What is the business user searching for? Claims values. Here's a possible implementation of FillSearch:
protected override void FillSearch(Uri context,
string[] entityTypes, string searchPattern,
string hierarchyNodeID, int maxCount,
SPProviderHierarchyTree searchTree)
{
// Look for a match on searchPattern text
// Find/create appropriate child node(s) in searchTree
// When a searchPattern matches, create a PickerEntity,
// add it to the appropriate node.
// hierarchyNodeID will identify the scope to be searched
// A null hierarchyNodeID indicates no node was
// selected, an empty one indicates the Provider node,
// the rest will have a Claim Type
string searchText = searchPattern.ToLower();
PickerEntity pickerEntity = default(PickerEntity);
string groupName = default(string);
SPProviderHierarchyNode node = default(SPProviderHierarchyNode);
EnsureGeoData();
SearchScope scope = SearchScope.All;
if (IsACountryClaimType(hierarchyNodeID))
{ scope = SearchScope.Country; }
else if (IsARegionClaimType(hierarchyNodeID))
{ scope = SearchScope.Region; }
else if (IsAProvinceClaimType(hierarchyNodeID))
{ scope = SearchScope.Province; }
// search all
var found = Geo.Descendants()
.Where(d => d.HasAttributes &&
d.Attribute("name").Value.ToLower().Contains(searchText));
string cType = default(string);
foreach (var geoRegion in found)
{
var parents = geoRegion.Ancestors().Where(a => a.HasAttributes);
switch (parents.Count())
{
case 0: // country
if (!
(scope == SearchScope.All ||
scope == SearchScope.Country))
{ continue; }
groupName = GetHierarchyNodeNameForNodeID(CountryClaimType);
pickerEntity = CreatePickerEntity(CountryClaimType, geoRegion.Attribute("name").Value,
CountryClaimValueType, groupName);
node = GetNodeForClaimType(searchTree, CountryClaimType, false);
node.AddEntity(pickerEntity);
break;
case 1: // region
if (!
(scope == SearchScope.Region ||
scope == SearchScope.All))
continue;
cType = GetRegionClaimType(parents.First().Attribute("name").Value);
if (scope == SearchScope.Region && cType != hierarchyNodeID)
continue;
groupName = GetHierarchyNodeNameForNodeID(cType);
pickerEntity = CreatePickerEntity(cType, geoRegion.Attribute("name").Value,
RegionClaimValueType, groupName);
node = GetNodeForClaimType(searchTree, cType, false);
node.AddEntity(pickerEntity);
break;
case 2: // province
if (!
(scope == SearchScope.Province ||
scope == SearchScope.All))
continue;
cType = GetProvinceClaimType(
parents.ElementAt(1).Attribute("name").Value,
parents.ElementAt(0).Attribute("name").Value);
groupName = GetHierarchyNodeNameForNodeID(cType);
if (scope == SearchScope.Province && cType != hierarchyNodeID)
continue;
pickerEntity = CreatePickerEntity(cType, geoRegion.Attribute("name").Value,
ProvinceClaimValueType, groupName);
node = GetNodeForClaimType(searchTree, cType, true);
node.AddEntity(pickerEntity);
break;
}
}
}
// Helper method
private SPProviderHierarchyNode GetNodeForClaimType(
SPProviderHierarchyTree searchTree, string claimType,
bool isLeaf)
{
SPProviderHierarchyNode node = default(SPProviderHierarchyNode);
if (searchTree.HasChild(claimType))
{
node = searchTree.GetChild(claimType);
}
else
{
node = CreateHierarchyNodeForNodeID(claimType, isLeaf);
searchTree.AddChild(node);
}
return node;
}
The search attempts to match the text the user typed into the Find textbox with all values from the GeoRegion datasource. For all matches/partial matches, the code figures out the level of the hierarchy for that match by finding the equivalent node in the GeoRegion datasource, then checking on the XML ancestors of that node. It constructs a Claim Type from those ancestors. Then it checks whether the current search scope (the hierarchyNodeID/ClaimType from the node that the user clicked) agrees with the constructed Claim Type. If so, the value is within the search scope, and it is just a matter of creating a PickerEntity and adding it to the correct node of the search tree. If the search tree node does not already exist, then it is created by helper method GetNodeForClaimType.
Note that in this implementation, the search results use a flat hierarchy in the left pane, in order to make the UI more intuitive.
Here's why. For search results, each node in the search tree control shows the total number of results for everything below and including that node, and the number shown can be a little confusing if the hierarchy tree is created. For example, searching for "a", and building the left pane hierarchy would look like this:

Hmmm - there are not 55 countries! There are a total of 55 results representing states, regions, and countries in the Country node and all the child nodes under it.
If the search builds a flat hierarchy, the visual display seems more natural, as seen in this screenshot, where there are 2 results for Country, 4 regions in the USA, 8 states in the USA-North region, using the same search for "a":

Note that after searching, clicking any node in the left pane will refresh the right pane with just the results for that node's scope - and this happens client-side.
Authorization
Authorization using the returned Claim Value works the same way as for Domain Groups or FBA Roles- pick a geographic region in the Claims Picker, and assign it to a SharePoint Group, or assign it permissions directly. It shows up like this on the Permissions page:

SharePoint treats Claims as if they were Domain Groups, hence the Type in the image above. In the screenshot, all users with a Claim Value of USA-South are authorized at the Design permission level. Since each user has three geographic region claims, this can become a broader scope (Country), or finer (Province). And, best of all, no need for an administrator to manually manage a multitude of SharePoint groups. When the user's data is updated in Active Directory (or wherever the location data lives), then the custom Claims Provider will automatically create new Claims at login time that match the new user data. That's a win-win!
Mar
28
Published: March 28, 2011 07:03 AM by
Nancy Brown
Part one presented a quick overview of Claims in SharePoint 2010: Claims-mode authentication, Claims Providers, Claims tokens. Claims Providers have two roles: Claims augmentation and Claims surfacing. Part two covers augmentation.
Implementing Hierarchical Claims
The goal of the Mindsharp GeoRegion custom Claims Provider is to provide the geographic region of the authenticated Windows user. Each authenticated Windows user will receive three additional (augmented) Claims: one for country, one for region within country, and one for state or province within a region. This provides granularity for permission setting: users can be authorized by large, medium or small regions.
Since these Claims are automatically generated by the Claims Provider, the user's Claims, and resulting permissions, are updated as soon as the user's data is updated. If the same kind of authorization were attempted by assigning the user to SharePoint groups, there would be many groups to manage, and each time a user moved to a new region, someone would have to manually move the user out of several SharePoint groups and into new ones.

To keep these posts to a reasonable length, and focus on hierarchy implementation rather than deployment details or the nitty-gritty of such things as database queries, only the most relevant points will be covered, and code samples will sometimes contain comments describing a process, rather than specific C# code. More information on Claims Provider development and deployment can be found in a number of online tutorials and articles. See http://msdn.microsoft.com/en-us/library/gg430136.aspx for starters. Please note that information about Claims Providers is still being developed; the following implementation of hierarchy is the result of the author's research, explorations, and guesses…
Base Class and Properties
Custom Claim Providers extend the abstract base class SPClaimProvider. That class includes several Boolean properties to indicate what functionality is being supported.
The Boolean property, SupportsEntityInformation, should be set to True to indicate Claims augmentation is supported.
The following three Boolean properties indicate whether (and how) the Claims Provider surfaces its Claims in the Claims Picker:
- SupportsResolve (Check Names)
- SupportsSearch (Select People and Groups -- Webpage Dialog)
- SupportsHierarchy (hierarchy levels on Select People and Groups -- Webpage Dialog)
All of these should be set to True when implementing Claims Hierarchies.
Supported Claim Types and Claim Value Types
Claims Providers must describe the Claim Types they support and the Value Types for each, through two overridden methods: FillClaimTypes() and FillClaimValueTypes(). These two methods must match the Claim Type and Value Type one-for-one. For example if a Claims Provider supports these:
|
Claim Type |
Value Type |
|
http://myDomain/2011/03/claims/GraduationDate |
http://www.w3.org/2001/XMLSchema#dateTime |
|
http://myDomain/2011/03/claims/IsAlumni |
http://www.w3.org/2001/XMLSchema#boolean |
|
http://myDomain/2011/03/claims/Degree |
http://www.w3.org/2001/XMLSchema#string |
Then the code would look something like this:
protected override void FillClaimTypes(List<string> claimTypes)
{
if (null == claimTypes)
{
throw new ArgumentNullException("claimTypes");
}
claimTypes.Add("http://myDomain/2011/03/claims/GraduationDate");
claimTypes.Add("http://myDomain/2011/03/claims/IsAlumni");
claimTypes.Add("http://myDomain/2011/03/claims/Degree");
}
protected override void FillClaimValueTypes(List<string> claimValueTypes)
{
if (null == claimValueTypes)
{
throw new ArgumentNullException("claimValueTypes");
}
claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.Datetime);
claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.Boolean);
claimValueTypes.Add(Microsoft.IdentityModel.Claims.ClaimValueTypes.String);
}
The point is that these match by index: if claimTypes[0] is "http://myDomain/2011/03/claims/GraduationDate", then the matching claimValueTypes[0] is "http://www.w3.org/2001/XMLSchema#dateTime".
For the GeoRegion provider, the Claim Types are themselves hierarchical. Fortunately, URLs are innately so! These will be built dynamically by embedding the Claim Value from the parent level into the Claim Type of the child level to create a new Claim Type which is self-describing. Here are a few examples:
|
Claim Type |
Claim Value |
Claim Value Type |
|
http://sharepointdevelopers.net/2011/02/claims
/country |
USA |
http://www.w3.org/2001/XMLSchema#string |
|
http://sharepointdevelopers.net/2011/02/claims
/country#USA/region |
USA-North |
http://www.w3.org/2001/XMLSchema#string |
|
http://sharepointdevelopers.net/2011/02/claims
/country#USA/region |
USA-East |
http://www.w3.org/2001/XMLSchema#string |
|
http://sharepointdevelopers.net/2011/02/claims
/country#Canada/region |
Canada-East |
http://www.w3.org/2001/XMLSchema#string |
|
http://sharepointdevelopers.net/2011/02/claims
/country#USA/region#USA-North/province |
Minnesota |
http://www.w3.org/2001/XMLSchema#string |
|
http://sharepointdevelopers.net/2011/02/claims
/country#USA/region#USA-South/province |
Virginia |
http://www.w3.org/2001/XMLSchema#string |
For the GeoRegion provider, the FillClaimsTypes method runs through the datasource for the geographical regions (an XML file), creates all possible Claim Types and adds them to the List. Since all of these are of type http://www.w3.org/2001/XMLSchema#string, method FillClaimsValueTypes need only add the string Value Type n times, where n is the number of Claim Types returned by method FillClaimsTypes.
protected override void FillClaimTypes(List<string> claimTypes)
{
if (null == claimTypes)
{
throw new ArgumentNullException("claimTypes");
}
claimTypes.Add(CountryClaimType);
claimTypes.AddRange(GetAllRegionClaimTypes());
claimTypes.AddRange(GetAllProvinceClaimTypes());
}
The geographic region datasource is an XML file which looks like this:
Claims Augmentation
Actual Claims augmentation is provided by overridden method FillClaimsForEntity. The code would look something like this:
protected override void FillClaimsForEntity(Uri context,
SPClaim entity, List<SPClaim> claims)
{
if (null == entity)
{ throw new ArgumentNullException("entity"); }
if (null == claims)
{ throw new ArgumentNullException("claims"); }
// Augment Claims only if Windows Authenticated User
if (!IsUserWindowsAuthenticated(entity.Value))
return;
Hashtable User = GetUserData(entity.Value);
claims.Add(GetCountryClaimForUser(User));
claims.Add(GetRegionClaimForUser(User));
claims.Add(GetProvinceClaimForUser(User));
}
private bool IsUserWindowsAuthenticated(string login)
{
if (login.Contains("#.w|"))
return true;
else
return false;
}
Since SharePoint 2010 allows multiple authentication types in one Zone, the authenticated user might be a Windows user, might be a Forms user, might be a Trusted Identity Provider user. So, the first thing this method should do is check for what kind of user, and only augment claims if that kind of user is one it supports. In this method, parameter SPClaim entity refers to the identity claim for the authenticated user. For a Windows user, the identity Claim value will look something like "0#.w|o14\user1", which is an encoded prefix (0#.w), followed by the login. In contrast, for a Forms user, the value would look something like "0#.f|mindsharpmembership|user1", which is an encoded prefix(0#.f), followed by the name of the Membership Provider (mindsharpmembership), then the user id (user1). The pipe character (|) is used to separate fields.
The helper method, GetUserData(entity.Value), performs a lookup in the datasource for the user, using entity.Value as the user's identity, finding the user's location, and returning a HashTable with keys "Country", "Region" and "Province". This might be an Active Directory lookup, or a query into a special geographic location database, or something else. This method will retrieve the user's Claim Values.
The methods GetCountryClaimForUser, GetRegionClaimForUser and GetProvinceClaimForUser will create the appropriate SPClaim for each hierarchical level, using the values in Hashtable User. These employ the SPClaimProvider base class method CreateClaim - this base class method should always be used - more on that later.
To facilitate creating Claim Types, some string constants are established first.
Here's an example of Claim-creating code:
// Alias Microsoft.IdentityModel to make this more readable
using ID = Microsoft.IdentityModel;
// Constants to aid Claim creation
internal const string ProviderClaimBase
= "http://SharepointDevelopers.net/2011/02/claims/";
internal const string CountryClaimType = ProviderClaimBase + "Country";
internal const string RegionSubClaimType = "/Region";
internal const string ProvinceSubClaimType = "/Province";
internal const string CountryClaimValueType = ID.Claims.ClaimValueTypes.String;
internal const string RegionClaimValueType = ID.Claims.ClaimValueTypes.String;
internal const string ProvinceClaimValueType = ID.Claims.ClaimValueTypes.String;
// example of Claim creation method
private SPClaim GetProvinceClaimForUser(Hashtable user)
{
string provinceClaimType = GetProvinceClaimType((string)user["Country"],
(string)user["Region"]);
return CreateClaim(provinceClaimType, (string)user["Province"], ProvinceClaimValueType);
}
private string GetProvinceClaimType(string country, string region)
{
return CountryClaimType + "#" + country
+ RegionSubClaimType + "#" + region
+ ProvinceSubClaimType;
}
Example: If User["Country"] is "USA", User["Region"] is "USA-West", User["Province"] is "Washington",
then GetProvinceClaimType returns "http://sharepointdevelopers.net/2011/02/claims/Country#USA/Region#USA-West/Province".
SPClaimEntityTypes, or "How is this to be used?"
The Claims Provider must report the usage of the Claims it is providing. The terminology gets a little confusing at this point. Earlier, the "entity" in the FillClaimsForEntity method was the user. Not so in the FillEntityTypes() method; now we are talking about the Claims themselves, and how they are to be used. Since the GeoRegion Claims will be used for authorization, they function like FBA roles, and that is the only SPClaimEntityType to be returned:
protected override void FillEntityTypes(
List<string> entityTypes)
{
// This enum value identifies the kind of augmentation.
// Other enum values include User, Distribution List,
// Security Group, System, and Trusted Claims Provider.
entityTypes.Add(SPClaimEntityTypes.FormsRole);
}
Now there are just the three Claims-Picker-surfacing methods to implement in part 3.
Mar
24
Published: March 24, 2011 19:03 PM by
Nancy Brown
Ever wish for a range of granularity in setting permissions in SharePoint? Wouldn't it be great to somehow automate the process of keeping users in the correct groups for authorization? Have a look at SharePoint 2010 custom Claims Providers using hierarchical Claims.
A brief bit of background
Custom Claims Providers are only available in Claims-mode Web Applications, which are SharePoint 2010 Web Applications created in or converted to Claims Based Authentication mode. Previously in SharePoint, the standard underlying identity token was a Windows identity token - that's Classic-mode. In SharePoint 2010, a more extensible model was adopted using SAML Claims tokens as the underlying identity token - that's Claims-mode.
SharePoint 2010 Claims-mode sites have many built-in Claims Providers. If a Claims Provider supports search, it is found on the left pane of the search page of the People Picker (AKA Claims Picker). And if the Claims Provider supports hierarchy, then level one of the hierarchy will appear when the Claims Picker's search page, Select People and Groups -- Webpage Dialog, first loads. In the image below, the Mindsharp GeoRegion Claims Provider is a custom Claims Provider, and all the others are built-in to SharePoint 2010.
A custom Claims Provider may also (optionally) provide resolve functionality- that's the familiar Check Names capability in the Claims Picker.
Custom Claims Providers are somewhat similar to Membership providers in that they inherit an abstract base class and are pluggable providers. However, Claims Providers are specific to SharePoint, not Asp.net, are typically registered with the Farm by a specialized Feature Receiver (a special base class) and may be scoped to specific Web Application Zones by PowerShell or custom code. A Claims Provider has two roles: adding (augmenting) claims, and optionally surfacing the Claims it adds in the People Picker (Claims Picker), by any of the three methods just described: resolve, search, or hierarchy.
Claims Augmentation occurs at login time, after authentication, and before authorization. At that point, the custom Claims Provider inspects the Claims token of the authenticated user, determines if the user is one it supports, and if so, attempts to add its new Claims to the user's security token. Generally, that means the Claims Provider will need access to the underlying datastore for the user, or that there will be existing Claims in the user's Claims token with the information needed to create the new Claims.
Why did SharePoint adopt SAML-token Claims authentication as its primary choice? SharePoint Claims are built on Windows Identity Foundation (WIF), which supports the open standards WS-Federation, WS-Trust, and WS-Security. This opens the door to authenticating users with LiveID, OpenID, Google, FaceBook and other existing logins, or federating identities with other SharePoint farms, in a consistent way.
Multiple authentication types in the same Zone are now a standard option for SharePoint, as evidenced by this screenshot of Authentication Types group of the Edit Authentication page in SharePoint's Central Administration:
Wait, what's a Claims token? A Claims token is a collection of Claims about an entity, which might be a user; the entity might also be a computer or something else. There is one primary claim- the identity claim, and all other claims are just additional assertions about that identity. There are five parts to a SharePoint Claim:
- Type
- Value
- Value type (data type)
- Issuer
- Original Issuer
Inspecting a SharePoint Claims token will reveal the Issuer is usually SharePoint, and the Original Issuer will be one of these: SharePoint, Windows, SecurityTokenService, ClaimProvider:[ClaimProviderName], TrustedProvider:[STSName], Forms:[MembershipOrRoleProviderName]. Value type is basically data type: String, DateTime, Integer, Boolean, etc.
Claim Types usually look like URLs; they must be unique, and a URL is a handy way to ensure that. A Claim value is just what it sounds like - the actual value, expressed as a string, since this is all being passed around the web as XML.
Here are some examples of individual Claims from a SharePoint SAML Claims token:
|
Claim Type |
Claim Value |
Claim Value Type |
Original Issuer |
Issuer |
|
http://schemas.xmlsoap.org
/ws/2005/05/identity/claims
/nameidentifier |
o14\admin |
http://www.w3.org/2001
/XMLSchema#string |
SharePoint |
SharePoint |
|
http://schemas.microsoft.com
/ws/2008/06/identity/claims
/primarysid |
S-1-5-21-1895496023-1035174427-2997398718-1000 |
http://www.w3.org/2001
/XMLSchema#string |
Windows |
SharePoint |
|
http://schemas.microsoft.com
/sharepoint/2009/08/claims
/userid |
0#.w|o14\admin |
http://www.w3.org/2001
/XMLSchema#string |
Security Token Service |
SharePoint |
|
http://schemas.microsoft.com
/sharepoint/2009/08/claims
/farmid |
7fc7238a-400f-4814-9c51-09c730f81e15 |
http://www.w3.org/2001
/XMLSchema#string |
ClaimProvider:
System |
SharePoint |
|
| | | | |