Wednesday, October 14, 2009

Copy SPListItem.Version (SPListItemVersion) Part 3

Background and Considerations

A while back I wrote a blog that discussed the issues with copying SPListItems from one list to another. However I recently needed to create a utility and thought my old blog would solve the problem – I am unhappy to say it did not. It definitely unlocks the issue with copying SPListItems with versions however I just found a couple shortcomings of what I wrote. Let’s try again.

Here are some considerations I had to understand before starting to build this.

  • The SPListItem CopyTo() and CopyFrom() methods do not work after doing some research with Reflector.
  • You will need to need to loop over the versions backwards and add the versions of the list items in the destination list.
  • Moving documents is different than moving list items.
  • Recursively looping over items within a SPList or SPDocumentLibrary is not straight forward. You usually want to maintain the folder structure when moving items from one list to another. You cannot simply loop over all items in the SPList nor does a SPFolder object have a collect of items within it. Only easy way of achieving this is to use a CAML query to get all the items for a specific folder.
  • If you need to preserve the Created and Modified time stamps on the version items, you need to set the times correctly because they are stored as GMT in the SharePoint database.
  • If you want to move items cleanly into a new or existing list, I recommend writing code that will first remove all the items from the destination list, then remove all the content types destination list and finally add the needed content types back into the destination list. There are numbers of reasons why to do this. It is possible to write a routine to reconcile the content types from the source list to the destination list however that can be come complicated. The important thing to know is that if a column is missing in the destination list, the movement of the SPListItem or SPDocument item will fail. The code I have written is not dependant on the content type ID which is a good thing. This is because if the content types are defined within the SharePoint UI a unique GUID is created for that content type. If you are moving items across SharePoint servers, you cannot be guaranteed that the Content Type ID will be the same, but the column names and types should be the same.

Create Copy Folders Structure

I created a method called MoveFolderItems which will recreate the folder structure in a new library. All you need to do initiate it is something like the following.

MoveFolderItems(sourceList, sourceList.RootFolder, destList, destList.RootFolder);

As you can see in this method, it first gets all the items for a specified folder. Then it checks to see if the item is another folder or not. If so, it will create a new folder, otherwise it will move over the item depending.

        private static void MoveFolderItems(SPList sourceList, SPFolder sourceFolder, SPList destList, SPFolder destFolder)
{
//Query for items in the source folder
SPQuery query = new SPQuery();
query.Folder = sourceFolder;
SPListItemCollection queryResults = sourceList.GetItems(query);

foreach (SPListItem existingItem in queryResults)
{
if (existingItem.FileSystemObjectType == SPFileSystemObjectType.Folder)
{
Console.WriteLine(existingItem.Name);

//Create new folder item
SPListItem newSubFolderItem = newSubFolderItem = destList.Items.Add(destFolder.ServerRelativeUrl,
SPFileSystemObjectType.Folder, null);

//Set folder fields
foreach (SPField sourceField in existingItem.Fields)
{
if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
newSubFolderItem[sourceField.Title] = existingItem[sourceField.Title];
}
}

//Save the new folder
newSubFolderItem.Update();

if (newSubFolderItem.ModerationInformation != null)
{
//Update Folder Status
newSubFolderItem.ModerationInformation.Status = SPModerationStatusType.Approved;
newSubFolderItem.Update();
}

//Get the source folder and the new folder created
SPFolder nextFolder = sourceList.ParentWeb.GetFolder(existingItem.UniqueId);
SPFolder newSubFolder = destList.ParentWeb.GetFolder(newSubFolderItem.UniqueId);

//Recursive call
MoveFolderItems(sourceList, nextFolder,
destList, newSubFolder);
}
else
{
//Move the item
Console.WriteLine(existingItem.Name);

if (sourceList.BaseTemplate == SPListTemplateType.DocumentLibrary)
{
MoveDocumentItem(existingItem, destFolder);
}
else {
MoveItem(existingItem, destFolder);
}
}
}
}

Move SPListItem

Here is the code for the SPList item with its history. First we create the list item. Then we loop over the versions backwards and add each version into the destination list.

            private static void MoveItem(SPListItem sourceItem, SPFolder destinationFolder) {
//Create a new item
SPListItem newItem;

if (destinationFolder.Item != null)
{
newItem = destinationFolder.Item.ListItems.Add(
destinationFolder.ServerRelativeUrl,
sourceItem.FileSystemObjectType);
}
else {
SPList destinationList = destinationFolder.ParentWeb.Lists[destinationFolder.ParentListId];
newItem = destinationList.Items.Add(
destinationFolder.ServerRelativeUrl,
sourceItem.FileSystemObjectType);
}

//loop over the soureitem, restore it
for (int i = sourceItem.Versions.Count - 1; i >= 0; i--) {
//set the values into the new item
foreach (SPField sourceField in sourceItem.Fields) {
SPListItemVersion version = sourceItem.Versions[i];

if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
newItem[sourceField.Title] = version[sourceField.Title];
}
else if (sourceField.Title == "Created"
sourceField.Title == "Modified")
{
DateTime date = Convert.ToDateTime(version[sourceField.Title]);
newItem[sourceField.Title] = sourceItem.Web.RegionalSettings.TimeZone.UTCToLocalTime(date);
}
else if (sourceField.Title == "Created By"
sourceField.Title == "Modified By")
{
newItem[sourceField.Title] = version[sourceField.Title];
}
}

//update the new item with version data
newItem.Update();
}

//Get the new item again
SPList list = destinationFolder.ParentWeb.Lists[destinationFolder.ParentListId];
newItem = list.GetItemByUniqueId(newItem.UniqueId);
newItem["Title"] = sourceItem["Title"];
newItem.SystemUpdate(false);

if (sourceItem.Attachments.Count > 0)
{
//now get the attachments, they are not versioned
foreach (string attachmentName in sourceItem.Attachments)
{
SPFile file = sourceItem.ParentList.ParentWeb.GetFile(
sourceItem.Attachments.UrlPrefix + attachmentName);

newItem.Attachments.Add(attachmentName, file.OpenBinary());
}

newItem.Update();
}
}

Move Document

As I mentioned earlier, moving a document is a little bit different. Here is the code that will copy a document, metadata and versions over to a new library.

       private static void MoveDocumentItem(SPListItem sourceItem, SPFolder destinationFolder)
{
//loop over the soureitem, restore it
for (int i = sourceItem.Versions.Count - 1; i >= 0; i--)
{
Hashtable htProperties = new Hashtable();

//set the values into the new item
foreach (SPField sourceField in sourceItem.Fields)
{
SPListItemVersion version = sourceItem.Versions[i];

if (version[sourceField.Title] != null)
{
if ((!sourceField.ReadOnlyField) && (sourceField.Type != SPFieldType.Attachments))
{
htProperties[sourceField.Title] = Convert.ToString(version[sourceField.Title]);
}
else if (sourceField.Title == "Created"
sourceField.Title == "Modified")
{
DateTime date = Convert.ToDateTime(version[sourceField.Title]);
htProperties[sourceField.Title] = sourceItem.Web.RegionalSettings.TimeZone.UTCToLocalTime(date);
}
else if (sourceField.Title == "Created By"
sourceField.Title == "Modified By")
{
htProperties[sourceField.Title] = Convert.ToString(version[sourceField.Title]);
}
}
}

//Get the version of the document
byte[] document;
if (i == 0)
{
document = sourceItem.File.OpenBinary();
}
else
{
document = sourceItem.File.Versions.GetVersionFromLabel(
sourceItem.Versions[i].VersionLabel).OpenBinary();
}

//Create the new item. Overwriting it will treat is as a
//new item.
SPFile newFile = destinationFolder.Files.Add(
destinationFolder.Url + "/" + sourceItem.File.Name,
document,
htProperties,
true);

newFile.Item["Created"] = htProperties["Created"];
newFile.Item["Modified"] = htProperties["Modified"];
newFile.Item.UpdateOverwriteVersion();
}

}

Wednesday, October 7, 2009

.NET 4.0 WF Initial Impressions

A couple months ago I was asked some very direct questions about the viability of K2 and other such tools with .NET 4.0 and Dublin. I personally have just not have had lots of time to do go off and research this. However I attended a quick one hour virtual session put on by Microsoft for WF in .NET 4.0.

The big thing I found out is that the State Machine workflow will not available in the initial release of .NET 4.0. That was a big surprise to me. All you will have is Sequential and Flow Chart workflows. The presenter said that you can achieve something similar to a State Machine workflow by doing a Flow Chart workflow. This would lead me to believe that many of the workflow challenges we had with WF in MOSS 2007 have not been resolved.

They talked a little about Workflow Services and I found out that you cannot do as much with Workflow Services than what you can do with WF. I did not get any details on what those specifics were.

A lot of the discussion was about how ISV can use WF to augment their frameworks and even provide the ability to allow customizations into their products using visual tools. This is what I have been preaching for a while now. You cannot adopt WF as the business process automation platform for a company. It does not come anywhere close. It is a framework to build business process automation frameworks.

I have had conversations think where companies believe that since they have SharePoint to host their WF workflows and they believe that is all they need. In the long run your costs will be significantly hirer to maintain, extend upon and manage. I have a personal thing with WF in SharePoint because I do not like the fact that the workflows can only be tied to a piece of content. If a company wanted to do finance or accounting process automation (that would span across enterprise systems) the workflow instance would have to be tied to a SharePoint list item which is not even actor in the process itself. So ask, why do we need this SharePoint list item, it serves no real purpose in the process. Plus if someone deletes the item or the associated task, the process will just end. There is no reporting, and the list goes on.

The point is that WF in MOSS should be used to just manage content in SharePoint. It is not a good platform for human workflow – you really need to look at other tools if you need human workflow. Plus it really does not look like Microsoft is chasing after companies like K2 and Nintex and they should have a healthy future.

Monday, October 5, 2009

IIS 7 Kerberos Configuration

I have seen several questions come up on projects in the past three weeks where teams are trying to configure Kerberos with IIS 7. With IIS 6 we were used to just setting up the SPNs. Now with IIS 7 we have to configure the <windowsAuthententication> node in the applicationHost.config file. If not, it will seem as if Kerberos is just flat out not working.

I have sent this blog to a couple of co-workers (http://sharepointspot.blogspot.com/2008/12/sharepoint-kerberos-on-windows-2008.html) and this got them up and running immediately.

If you want a little background Kerberos configuration in general – read this blog I wrote - http://www.k2distillery.com/2009/04/k2-blackpearl-kerberos-configuration.html. Most of the content is slighted towards K2 configuration with Kerberos however it will help you if you never done it before.

This blog (http://blogs.msdn.com/martinkearn/archive/2007/04/23/configuring-kerberos-for-sharepoint-2007-part-1-base-configuration-for-sharepoint.aspx) is probably the most well known blog on Kerberos for MOSS. This guy basically shows you all of the Kerberos commands that you need to run for all the SharePoint service accounts that you may create for your SharePoint farm.

As well, Kerberos configuration comes up a lot with the configuration of SSRS and MOSS. Here is a good article that explains it (http://msdn.microsoft.com/en-us/library/bb283324.aspx).

Saturday, October 3, 2009

Embed and Deploy User Control in SharePoint Web Part

1.0 Introduction

Several months I go I had some colleagues mention to me that it is possible load an ASP.net user control into a SharePoint web part. You may be asking why would I want to or consider doing that. There are some important reasons.

  • Your company may already have a large investment in standard ASP.net user controls and you do not want to have to rewrite them as a SharePoint web part.
  • ASP.net user controls can be easily embedded into other custom web applications.
  • SharePoint web part development can be challenging at times to build up a rich user interface. Using an ASP.net user control, you can build and test that code outside of the SharePoint context. I believe that most of this has to do with short comings of Visual Studio as an Integrated Development Environment (IDE) for SharePoint. We expect great things soon…

A popular code project call SmartPart is out there which many people have used to load user controls into a web part. Greg Galipeau referred me to blog which discussed many short comings of that project. Upon reading that, I knew I would never us it for a client and that it is really not that hard to create your own SmartPart web parts.

As I have discussed in the past, I am a huge proponent of:

  1. Creating SharePoint deployment projects that deploy everything in a solution and Feature.
  2. Anything that is deployed to SharePoint runs under minimal trust.

In this article I plan to show you how to create a .Net project that builds a .NET user control and web part, how to deploy the solution and best practices I learned along the way.

2.0 Creating the SharePoint Projects

There are basically two projects we need to create. The first if for the .NET user control and the second is for the ASP.net web part which will load the user control. The process I am going to take you through is:

  1. Build the ASP.net Project by itself.
  2. Create a Web Part Project.
  3. Then show you the modifications to integrate the user control into the web part.

2.1 Creating the ASP.net Project

First create the project for the ASP.net control. This is as simple as creating an ASP.net project. Here is a screen shot of the project that I created.

image

There is really nothing special about it:

  • I left the Default.aspx so that I can use it for testing the SmartControl.
  • I please the SmartControl in a UserControls folder. No specific reason other than come practice.

Here is the code from SmartContro.ascx. Note I only have a simple label we will use for testing purposes.

<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="SmartControl.ascx.cs" Inherits="MOSSDistillery.SmartControl.UserControl.UserControls.SmartControl" %>
<asp:Label ID="lblHelloWorld" runat="server" Text=""></asp:Label>

Here is the code behind for SmartControl.ascx.cs. The only interesting thing I have done here is created a method for changing the color of the text of the label. In the example later on, I will show how this can be set from the web part’s configuration. The point of this is to show how to pass data into the user control.

public partial class SmartControl : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
lblHelloWorld.Text = "Hello World";
lblHelloWorld.ForeColor = System.Drawing.ColorTranslator.FromHtml("#FFFF00");
}
}

2.2 Creating the Web Part Project

Second create the project for the web part. I know that I could use WSPBuilder to make my life easier however I have found that it is really not that hard to build a Feature and web part. In my blog on how to create web part, I provided the exact steps on how to project for a web part project. Please go there and complete the instructions, as that is what I have done.

Here is my resulting project as well as the code for my web part project using the instructions I have in this blog.

image

SmartWebPart.cs

public class SmartWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
protected override void Render(HtmlTextWriter writer) {
base.Render(writer);
writer.WriteLine("Hello World");
}
}

MOSSDistillery.SmartControl.WebPart.SimpleWebPart.webpart

<webParts>
<webPart xmlns="http://schemas.microsoft.com/WebPart/v3">
<metaData>
<type name="MOSSDistillery.SmartControl.WebPart.SmartWebPart, MOSSDistillery.SmartControl.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e652952dcf5e6363" />
<importErrorMessage>Cannot import MOSSDistillery.SmartControl.WebPart.SmartWebPart</importErrorMessage>
</metaData>
<data>
<properties>
<property name="Title" type="string">My Smart Web Part</property>
<property name="Description" type="string">My Smart Web Part Demonstration.</property>
</properties>
</data>
</webPart>
</webParts>

Feature.xml

<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="21E3F9D4-6DC2-4042-A873-C3440127476F"
Title="My Smart Web Part"
Description="My Smart Web Part Demonstration."
Version="1.0.0.0"
Scope="Site"
Hidden="FALSE"
DefaultResourceFile="core"
xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementManifest Location="elements.xml" />
<ElementFile Location="MOSSDistillery.SmartControl.WebPart.SimpleWebPart.webpart"/>
</ElementManifests>
</Feature>

elements.xml

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
<Module Name="WebParts" List="113" Url="_catalogs/wp">
<File Url="MOSSDistillery.SmartControl.WebPart.SimpleWebPart.webpart" Type="GhostableInLibrary" />
</Module>
</Elements>

manifest.xml

<Solution xmlns="http://schemas.microsoft.com/sharepoint/" SolutionId="1B69F425-BCC3-4de0-BC2F-A84B1168F84A">
<FeatureManifests>
<FeatureManifest Location="MOSSDistillery.SmartControl.WebPart\Feature.xml"/>
</FeatureManifests>
<Assemblies>
<Assembly Location="MOSSDistillery.SmartControl.WebPart\MOSSDistillery.SmartControl.WebPart.dll" DeploymentTarget="GlobalAssemblyCache" >
<SafeControls>
<SafeControl Assembly="MOSSDistillery.SmartControl.WebPart.SmartWebPart, MOSSDistillery.SmartControl.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e652952dcf5e6363"
Namespace="MOSSDistillery.SmartControl.WebPart"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
<CodeAccessSecurity>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="..." />
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="MOSSDistillery.SmartControl.WebPart" version="1" Description="MOSSDistillery.UserControl.WebPart">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Unrestricted="true" />
<IPermission class="WebPartPermission" version="1" Connections="True" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="true" />
</PermissionSet>
</PolicyItem>
</CodeAccessSecurity>
</Solution>

WSP.ddf

.OPTION Explicit
.Set CabinetNameTemplate="MOSSDistillery.SmartControl.WebPart.wsp"
.Set DiskDirectory1="C:\MOSSDistillery\SmartControl\MOSSDistillery.SmartControl\MOSSDistillery.SmartControl.WebPart\Deployment"

manifest.xml

.Set DestinationDir="MOSSDistillery.SmartControl.WebPart"
%outputDir%MOSSDistillery.SmartControl.WebPart.dll
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\elements.xml
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\Feature.xml
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\MOSSDistillery.SmartControl.WebPart.SimpleWebPart.webpart

.Delete outputDir

2.3 Preparing for Project Deployment

Now that we have both the projects created, we need to complete the following steps to integrate them so that we can display the user control within the web part.

2.3.1 Sign the ASP.net Project

First we need to sign the ASP.net project with the user control because it will be deployed to the GAC.

2.3.2 Get the Public Key Token

Then you will need to run the following command like we did for the web part project so we can get the public key token.

sn -Tp "C:\MOSSDistillery\SmartControl\MOSSDistillery.SmartControl\MOSSDistillery.SmartControl.UserControl\bin\MOSSDistillery.SmartControl.UserControl.dll"

2.3.3 Modify SmartControl.ascx

First add the Assembly tag to the user control. This is so the web control can reference the dll that will be deployed to the GAC. Notice we used the public key token we created in the previous step. Second, I removed the CodeBehind attribute in the Control element.


<%@ Assembly Name="MOSSDistillery.SmartControl.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=367c68a97c663918"%>
<%@ Control Language="C#" AutoEventWireup="true" Inherits="MOSSDistillery.SmartControl.UserControl.SmartControl" %>
<asp:Label ID="lblHelloWorld" runat="server" Text=""></asp:Label>

2.3.4 Change the User Control Code

All I did was create some properties which set the text and the color. We will set these from the SharePoint web part.

public partial class SmartControl : System.Web.UI.UserControl
{
private string _text = string.Empty;
private string _color = string.Empty;

public string Color
{
get
{
return _color;
}
set
{
_color = value;
}
}

public string Text
{
get
{
return _text;
}
set
{
_text = value;
}
}

protected void Page_Load(object sender, EventArgs e)
{
lblHelloWorld.Text = _text;
lblHelloWorld.ForeColor = System.Drawing.ColorTranslator.FromHtml(_color);
}
}


2.3.5 Modify the Web Part

Now I have to modify the web part to set the properties of my user control. The core of this solution is the code I put in the CreateChildControls() method.

public class SmartWebPart : System.Web.UI.WebControls.WebParts.WebPart
{
private string _error = string.Empty;
private string _color = string.Empty;
private string _text = string.Empty;

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
WebDisplayName("Text"),
WebDescription("Text"),
Category("Custom")]
public string Text
{
get
{
return _text;
}
set
{
_text = value;
}
}

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
WebDisplayName("Color"),
WebDescription("Color"),
Category("Custom")]
public string Color
{
get
{
return _color;
}
set
{
_color = value;
}
}

protected override void Render(HtmlTextWriter writer) {
base.Render(writer);
writer.WriteLine(_error);
}

protected override void CreateChildControls()
{
try
{
base.CreateChildControls();

if (string.IsNullOrEmpty(_text))
{
throw new Exception("No text has not been set");
}

if (string.IsNullOrEmpty(_color))
{
throw new Exception("Color has not been set");
}

string path = "~/_controltemplates/MOSSDistillery.SmartControl.UserControl/SmartControl.ascx";
MOSSDistillery.SmartControl.UserControl.SmartControl control =
(MOSSDistillery.SmartControl.UserControl.SmartControl)Page.LoadControl(path);

control.Text = _text;
control.Color = _color;

Controls.Add(control);
}
catch (Exception ex)
{
_error = ex.Message + " " + ex.InnerException;
}
}
}

One important note, notice that I hard coded the path to the user control. If I wanted to make this a generic web part that would have the ability to load any ASP.net user control, I would instead make the following changes. The problem with this approach is that configuration values from the web part could not be set into the user control. It possible to use the web.config but again in this case it did not make sense because a web part like this could be used in lots of places. In each of those places the color or text may be different so a web.config setting would not work. It could be possible to create some code that uses reflection to set the properties of ASP.net user control but I really did not want to create anything that complex yet.

string _path = "";

[Personalizable(PersonalizationScope.Shared),
WebBrowsable(true),
WebDisplayName("User Control Path"),
WebDescription("User Control Path"),
Category("Custom")]
public string UserControlPath {
get {
return _path;
}
set {
_path = value;
}
}
protected override void CreateChildControls()
{
try
{
base.CreateChildControls();

if (string.IsNullOrEmpty(_path))
{
throw new Exception("Path has not been set");
}

System.Web.UI.UserControl control = (System.Web.UI.UserControl)Page.LoadControl(_path);
Controls.Add(control);
}
catch (Exception ex)
{
_error = ex.Message + " " + ex.InnerException;
}
}

2.3.6 Change Manifest.xml

Next we need to make the following changes to the manifest.xml. Basically we need to incorporate the user control into the deployment.

  1. We add the UserControl as a new Assembly element. It is important to add the SafeControls element for the UserControl so that it will be marked as a safe control in the web.config.
  2. We add TemplateFiles element which will place the ascx control in the CONTROLTEMPLATES folder in the 12 hive. Notice that the location has “MOSSDistillery.SmartControl.UserControl”. This will create a folder called “MOSSDistillery.SmartControl.UserControl” and will place the ascx control in that folder. This is important so your user controls do not get intermingled with the out of the box MOSS user controls.
<Solution xmlns="http://schemas.microsoft.com/sharepoint/" hSolutionId="1B69F425-BCC3-4de0-BC2F-A84B1168F84A">
<FeatureManifests>
<FeatureManifest Location="MOSSDistillery.SmartControl.WebPart\Feature.xml"/>
</FeatureManifests>
<Assemblies>
<Assembly Location="MOSSDistillery.SmartControl.WebPart\MOSSDistillery.SmartControl.WebPart.dll" DeploymentTarget="GlobalAssemblyCache" >
<SafeControls>
<SafeControl Assembly="MOSSDistillery.SmartControl.WebPart.SmartWebPart, MOSSDistillery.SmartControl.WebPart, Version=1.0.0.0, Culture=neutral, PublicKeyToken=e652952dcf5e6363"
Namespace="MOSSDistillery.SmartControl.WebPart"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
<Assembly Location="MOSSDistillery.SmartControl.UserControl\MOSSDistillery.SmartControl.UserControl.dll" DeploymentTarget="GlobalAssemblyCache">
<SafeControls>
<SafeControl Assembly="MOSSDistillery.SmartControl.UserControl.SmartControl, MOSSDistillery.SmartControl.UserControl, Version=1.0.0.0, Culture=neutral, PublicKeyToken=367c68a97c663918"
Namespace="MOSSDistillery.SmartControl.UserControl"
Safe="True"
TypeName="*"/>
</SafeControls>
</Assembly>
</Assemblies>
<TemplateFiles>
<TemplateFile Location="CONTROLTEMPLATES\MOSSDistillery.SmartControl.UserControl\SmartControl.ascx"/>
</TemplateFiles>
<CodeAccessSecurity>
<PolicyItem>
<Assemblies>
<Assembly PublicKeyBlob="..." />
</Assemblies>
<PermissionSet class="NamedPermissionSet" Name="MOSSDistillery.SmartControl.WebPart" version="1" Description="MOSSDistillery.UserControl.WebPart">
<IPermission class="AspNetHostingPermission" version="1" Level="Minimal" />
<IPermission class="SecurityPermission" version="1" Unrestricted="true" />
<IPermission class="WebPartPermission" version="1" Connections="True" />
<IPermission class="Microsoft.SharePoint.Security.SharePointPermission, Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" version="1" Unrestricted="true" />
</PermissionSet>
</PolicyItem>
</CodeAccessSecurity>
</Solution>


2.3.7 Modify the WSP.ddf

Finally we needed to make a couple modifications to bring in the ASP.net User Control. You can see that I have pulled in both SmartControl.dll and SmartControl.ascx. Both of the paths to those files match the paths specified in the manifest.xml.

.OPTION Explicit
.Set CabinetNameTemplate="MOSSDistillery.SmartControl.WebPart.wsp"
.Set DiskDirectory1="C:\MOSSDistillery\SmartControl\MOSSDistillery.SmartControl\MOSSDistillery.SmartControl.WebPart\Deployment"

manifest.xml

.Set DestinationDir="MOSSDistillery.SmartControl.WebPart"
%outputDir%MOSSDistillery.SmartControl.WebPart.dll
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\elements.xml
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\Feature.xml
TEMPLATE\FEATURES\MOSSDistillery.SmartControl.WebPart\MOSSDistillery.SmartControl.WebPart.SimpleWebPart.webpart

.Set DestinationDir="MOSSDistillery.SmartControl.UserControl"
..\MOSSDistillery.SmartControl.UserControl\bin\MOSSDistillery.SmartControl.UserControl.dll

.Set DestinationDir="CONTROLTEMPLATES\MOSSDistillery.SmartControl.UserControl"
..\MOSSDistillery.SmartControl.UserControl\UserControls\SmartControl.ascx

.Delete outputDir


3.0 Conclusions

That is it; you can now see how easy it is to deploy a user control to SharePoint and load it into a web part. The great thing about this is that you can do development of web parts significantly more quickly.

4.0 References

Sunday, September 27, 2009

User Control Error In SharePoint

Background

I was getting the following error when trying to build a simple hello world user control (ascx) and then load it into a web part.

Error

The file '/_controltemplates/XXX/MyWebUserControl.ascx' does not exist.

at System.Web.UI.Util.CheckVirtualFileExists(VirtualPath virtualPath)

at System.Web.Compilation.BuildManager.GetVPathBuildResultInternal(VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)

at System.Web.Compilation.BuildManager.GetVPathBuildResultWithNoAssert(HttpContext context, VirtualPath virtualPath, Boolean noBuild, Boolean allowCrossApp, Boolean allowBuildInPrecompile)

at System.Web.UI.TemplateControl.LoadControl(VirtualPath virtualPath)

at System.Web.UI.TemplateControl.LoadControl(String virtualPath)

at MOSSDistillery.UserControl.WebPart.MySmartPart.CreateChildControls()

Symptoms

What was frustrating was that I could point to any existing user controls provided in SharePoint and they load in fine. Here is the code:

protected override void Render(HtmlTextWriter writer) {
base.Render(writer);
writer.WriteLine(loadControlError);
}
protected override void CreateChildControls()
{
try
{
base.CreateChildControls();

System.Web.UI.UserControl control = (System.Web.UI.UserControl)Page.LoadControl("~/_controltemplates/MyWebUserControl/MyWebUserControl.ascx");
Controls.Add(control);
}
catch (Exception ex)
{
loadControlError = ex.Message + " " + ex.InnerException;
}
}

This error was pretty tough to figure out because:

  • The ascx file was in the correct place.
  • I had entered a safe controls entry.
  • If I changed the web.config to Full trust, everything would work.

I did a significant amount of investigation to understand why I was getting this error. From what I was able to find out, you will get this error in the following scenarios.

  • If you have the ascx file in a location where it cannot be found.
  • If you place the web part dll in the web bin directory and do not place that ascx control in the ~/_controltemplates/ directory. Specifically if you place the web control in a custom folder within the control templates folder in the 12 hive folder. Even with all the correct CAS permissions, it will not work. However if the web part dll in the GAC, it will work.
  • If you place the web part dll in the web bin directory, the error will go away if you move the code from the CreateChildControls() to the OnInit().

Resolution

When research the solution, I saw that many people would just change the permissions levels to full which is a completely unacceptable solution. The following are some deployment options for a web part that will show an ASP.net user control. I have ordered them in the most secure to least secure:

  • Deploy the web part dll to the bin. Add the code to load the user control in the OnInit() method. Place the user control (ascx) directly into the control templates folder in the 12 hive. Make sure you have a safe controls entry in the web.config. Only disadvantage is that you cannot place the user control into a custom folder to separate your user control from SharePoint's user controls. However this is the most secure.
  • Deploy the web part dll to the GAC. Place the loading of the user control into the CreateChildControls method. Place the user control (ascx) where ever you want within the control templates folder in the 12 hive directory. I suggest putting the user control into a custom folder. Make sure you have a safe controls entry in the web.config. The disadvantage is that the dll is not accessible to any running process on the web server. I know many developers have become accustomed to putting dlls in the GAC but again you should always strive to deploy to the web bin and run under minimal trust.
  • I found this option - http://blogs.msdn.com/chandru/archive/2009/03/02/cas-in-sharepoint.aspx. It does work however what you are basically doing is allowing the custom CAS permission you have created for your web part to run in full trust. There are several major issues with doing this. First, the stsadm addsolution command will fail if Unrestricted="true" is added to the PermissionSet within the manifest.xml file – it is not allowed. Second, you decided to do this, will have to manually make the change to each front end web server in the farm which is a deployment problem. Third, you are allowing all dlls running under the CAS permission to run with full trust.
  • Run SharePoint in full trust – please do not do this – it is highly unrecommended.

Saturday, September 26, 2009

Excel and PDF Data Dump in SharePoint

Background

I recently had to come up with a quick solution in a week to provide both Excel and PDF downloads of datasets that I was being presented through some SharePoint web parts. I had to create some use controls that would be embedded into SharePoint that providing reporting data. The user needed the ability to down load the underlying datasets in either Excel or PDF.

My natural first inclination was to use SSRS because first it has tight integration with SharePoint. Second the report viewer control provides the ability to download the report and excel and pdf formats. Well in this case I could not use SSRS because it was not part of the solution architecture. As well, I needed to find a "free" solution.

Excel Solution

For Excel, I investigated a couple different solutions. I particularly did not want to have to mess around with the Office Excel API. First the API is not very clean and would be time consuming to code to. Second I would have to install Office onto the front end web servers. Then I thought, how about providing an XML file that opens into Excel? I though, InfoPath is just an XML file with specific processing tags embedded into it which would open InfoPath. I recalled that the same could be done with Excel. I subsequently found this blog posting (http://www.ksaelen.be/wordpress/2009/08/using-c-xml-xslt-to-create-excel-spreadsheet/) and it provided me with an extremely easy way of generating excel data dumps. All you basically do:

  • Create an Excel file with all the presentation you want.
  • Save it is as an Office 2003 XML format.
  • Change the XML file to an *.xsl file.
  • Make modifications to the .xsl file to merge in data from another XML file which has all the data.
  • The use the XslCompilatedTransform class to merge it together.
  • Then I took the results, put it into a MemoryStream and saved down into a document library.

The limitations of this solution are that any charting and code modules you may want to include in the generated XML file is not supported. This is really only a good solution for doing a data dump of the underlying data that may be presented to a user. However it is really simple to implement, requires no special add-ins, you can stylize your presentation and you can be provide very dynamic results. Plus many of the reports provided by SharePoint use XML Excel files so I did not feel too bad in doing this.

PDF Solution

The next thing I had to do was provide the same results in PDF format because not everyone would have Excel. Now the solution above will support both Office 2003 and 2007, however someone may not have Office installed at all. I looked into several tools that would do PDF generation from HTML, but I needed to keep cost down. I subsequently found a very mature open source PDF library called PDFsharp and MigradDoc Foundation:

The source code is very well written, commented and managed. This scored big points in my book because I usually shy away from open source solutions strictly for code maintenance reasons. These guys also provided tons of code samples and provide the ability to do basic charting too.

I was able to in an afternoon, pull in the library and generate tables of data into PDF. It was very easy for me to then save it down into a document library and provide a user a link to that generated PDF. I specifically used the MigraDoc API to save the generated PDF into a MemoryStream and then just push that right into a SPListItem.

Monday, September 7, 2009

Using old VHDs on Windows Virtual PC

Background


Couple weeks ago I put Windows 7 on my new laptop. As a developer one of the first things I had to do was take advantages of the new virtualization features provided by Windows 7. For the past five years I have been using Microsoft Virtual PC for everything. I have preached and preached it for the past couple years because:

  • I can create dedicated environments for projects and/or clients.
  • My environments will not dependencies installed within them that could implementations between environments.
  • I can save state and come back to something at any time.
  • It is really easy for take a snapshot and then do something risky or install some new flaky software.

I had mostly used Virtual PC 2007 SP1 and had become familiar with Virtual Server. When I put on Windows 7 on my machine; I really did not know a couple things had changed. I am not a desktop OS person and I had not read much about Windows 7 – other than I held off from Vista.


My goal is to have my host OS on 64 bit with Windows 7 and bring over all my old vhd drives and run them on this machine. You cannot run Virtual VPC 2007 on Windows 7, however the .vhd files created by Virtual VPC 2007 can be used with Windows Virtual PC on Windows 7.


Here are the key differences on the surface you should know when moving over to Windows 7 and using virtualization:

  1. It is now called "Windows Virtual PC"
  2. The interface is different from what it used to be.
  3. Microsoft is now using it as a strategy to allow every day desktop users use it to get backwards compatibility to applications that work on Windows XP.
  4. VHD files can now be mounted (attached) as a disk and then access the files within the VHD directly on the host machine windows explorer.
  5. It possible to boot the VHD as a duel boot.

Installation


Installation is not that hard. First go here and select information about your Windows 7 OS. You will need to minimally download Windows Virtual PC RC. Go ahead and then download Windows XP Mode RC. You can also get all the installation instructions right off this page.


Some important pre-requisites steps that you will need to complete are:


  1. System requirements - http://www.microsoft.com/windows/virtual-pc/support/requirements.aspx - Upon reading the system requirements you may note that only Guest Operating Systems of Windows XP, Windows Vista and Windows 7 are only supported. Do not be alarmed, this basically means you will not be able to take full advantage integration between the host OS and guest OS. I can say for the past couple weeks I have been able to bring over several of my VPCs running on Windows Server 2003 with no problem. I know others who have been able to bring over Windows Server 2008.
  2. You need to make sure that your BIOS are properly set up. Follow the instructions here - http://www.microsoft.com/windows/virtual-pc/support/configure-bios.aspx
  3. Install the Windows Virtual PC RC.
  4. Install Windows XP Mode RC (optional).

Windows Virtual PC


As I mentioned, the User Interface has changed as it is integrated within Windows Explorer. I am not going to give a tutorial on how to use it however; here is what it looks like. After the installation there will be a new folder called Virtual Machines created which is where you can access and your VPCs. As you can see all of the files and there is a new tool bar being displayed.



Taking a close look at the tool bar, when I select a .vmcx (which is equivalent to a .vmc for VPC 2007), you will see an "Open" option added to the tool bar. You can click on that or just double click the .vmcx file which will start up the VPC.

You will also notice that there is a "Settings" option in the tool bar when you select a .vmcx file. When you click on Setttings, you will get the traditional VPC Settings screen we are familiar with to configure memory, disk, networking, etc.



Finally in the tool bar, there is a "Create virtual machine" button which will launch a wizard to create a new VPC. This wizard is again very similar to Virtual VPC 2007.


You now see that using VPCs on Windows 7 is more of an integrated experience.


Windows Virtual PC >> Windows XP Mode


As I mentioned this is an optional installer. From what I can tell, the strategy by Microsoft it so use virtualization to allow backward support of applications. As well, the way this is marketed, it is more meant for the everyday desktop user. What it allows is a user to get a XP VPC running (without any licensing), to run older applications. I personally think it is great. Plus if you are worried about your kids or wife getting some virus while surfing or what not, have then surf on the VPC J


Running an Old VPC


Like I mentioned, I needed to run some vhd files I had created with Virtual VPC 2007 that had a guest OS of Windows Server 2003 (which is not supported). This is all you need to do:

  1. Move the vhd file over to your Windows 7 machine. I suggest that you create a folder under C:\Users\[username]\Virtual Machines\ and place the vhd there.
  2. Then go to C:\Users\[username]\Virtual Machines\ directory and click "Create virtual machine". This will initiate a wizard.
  3. Specify a name and change the location to point to the location where the vhd file is. Press Next.
  4. Specify RAM and press next.
  5. On the "Add a virtual hard disk", select the "Use an existing virtual hard disk" and select the vhd file that was moved over. Press the Create button and you are complete.

When this is completed, a .vmcx file should be created in C:\Users\[username]\Virtual Machines\. To boot the VPC, all you need to do is double click that file, or select Open button in the tool bar. I suggest verifying the configuration by pressing the Settings button before spinning it up.

Here are some things you may run into.

First you will see a pop-up saying "Updates for Windows Virtual PC Integration Components are available" every time you reboot the guest OS. DO NOT press the update button. This will install the integration components for Windows Virtual PC which is not compatible with either Windows Server 2003 or 2008. I tried it; it did not work. Just press the cancel button and you will be fine.


Second, you will constantly get a prompt after log in saying New Hardware has been found. Again, just cancel.


After that, you should be good to go.

One thing I have not tried is creating a brand new VPC with Windows Server 2003 or 2008 using Windows Virtual PC. I wonder how the integration between the Host and the Guest will work without installing the additions in the Guest OS; remember you cannot install the Windows Virtual PC additions. I guess the only option would be to run the vhd on a machine which had Virtual VPC 2007 SP1, install the additions, and then bring the vhd back to the Windows 7 machine.

Sharing Files

Because the integration components do not work with Windows Server 2003 and 2008 on a guest OS, it is not possible to drag and drop files between the host and guest. As well, if you go to the settings of the vmcx file, you will see the Integration Features option. Even if you try to select Drives here, they will not work.


The best way to accomplish this is to:

  • Create a loop back adapter on your host machine running Windows Server 2007.
  • Then click on the Networking settings for the vmcx. For the first adapter set it to NAT and for the second adapter select the Microsoft Loopback Adapter you just created on the host machine.
  • Then create a Shared Folder on the guest OS.
  • Then map to that folder of the guest machine from the host machine.

Experiencing Slowness?


If you are experiencing any slowness in the UI resolution of the VPC the following KB will help you out - http://support.microsoft.com/kb/899525. Even though it is a fix for Virtual VPC 2004, it does help with Windows Virtual PC. The location of the Options.xml file is %userprofile%\AppData\Local\Microsoft\Windows Virtual PC\


Mount a VHD Drive


Now this is a really cool new feature of virtualization with Windows 7. You now have the ability to treat a vhd file like a drive on the machine. Please read the following - http://thelazyadmin.com/blogs/thelazyadmin/archive/2009/01/15/mount-a-vhd-within-windows-7-server-2008-r2.aspx.


However you cannot do this while the vpc is running, so this is not a good open to share files between the host and guest OS if you need to do it on a regular basis.


One cool thing is if you do not want to boot up an old vhd to get some files of it, all you need to is mount it. You can go right into the vhd using your host's windows explorer and extract or use any of the files on there. I suspect people will start coming up with really creative ways to use this over the next couple of months.


Dual Boot from VHD


Now this is what many folks are blogging about, it is your ability to do a dual boot from your vhd. Please read here - http://blogs.technet.com/keithcombs/archive/2009/05/22/dual-boot-from-vhd-using-windows-7-and-windows-server-2008-r2.aspx.


Again I plan on taking advantage of this really soon to maximize memory usage because there is no need to run the host OS anymore…