SharePoint Automation

Abstract

When I meet with clients to do design sessions, I’m often asked why Microsoft made a certain SPS process this or that way. In particular, the team site creation process from within a SPS portal seems to be a popular one. Compounding this is my advice for planning ahead. During the design of SPS/WSS implementations, I often propose dedicating virtual servers for groups of collaborative sites. I also push clients to consider many top-level sites versus very deep site collections. All of this is to provide the opportunity for growth. Dedicating virtual servers makes backup and restore processes easier and increases the ability to deliver a different SLA for team sites versus the portal. Having many top-level sites opens the ability to control more of what goes in what content database. However, some of these recommendations for my clients make SPS harder to use. The “Create Site” process of the portal Site Directory now isn’t enough. I want my clients’ admins to be aware of what virtual server, under what managed path, and with what naming convention they should be using when creating a site. So recently, I put together an example that shows that my advice can actually make SPS easier to use with a little bit of custom dev up front.  

 

The Scenario

This example can really be applied to any industry, but my client was higher education based so here it goes. Let us say that there exists a subset of the users which are faculty members. These faculty members can create team sites for their classes. As an administrator, we’ve decided that we would like all of these sites in a virtual server that is separate from the portal. In fact, we have created a managed path “/classes” with a wildcard inclusion so that each class site can be a top level site under this path. Ideally, we would like to enforce that the top level site is placed within this URL with the class number (ex: /classes/12345). Also, the faculty member requesting the site should be designated as the administrator once the team site is created. Ideally, we would like an admin in IT to review any request by a faculty member just to confirm the validity of the request.

 

For the rest of the example, let us assume the portal is at: https://spsplay.sample.microsoft.com and the virtual server for the team sites is https://wss1.sample.microsoft.com.

 

Audience Targeting & My Site

So my approach to making this “easy” for the faculty member is to provide them with a wizard experience. I’m going to build my wizard later, but let us say for now that it will be contained in a team site named “Wizards”. In order to make this wizard easy to find, I took advantage of SPS audiences and “Links for You” functionality. These personalization tools enable me to place a link to my wizard on the My Site page of every faculty member. To make this work, I created a Faculty security group in my AD domain, added the users to it, and created a matching audience. I’m going to assume that these are basic steps and that you don’t need the blow by blow. Once I had an audience, I could use the “Manage targeted links on My Site” admin functionality to push my link to the faculty my sites.

 

Building Wizards

Now to show off in my demo, I wanted to illustrate all types of SPS development techniques. So, I took the opportunity to create my own site definition (by copying STS) and place a Visual Studio built aspx page there as the wizard. This could also easily be done with InfoPath, but a custom web part page built in VS.NET seemed like a challenge. How did I do it?

  • Created a custom site definition by copy STS and naming in MYWIZARDS.
  • Added the appropriate WEBTEMPNEW.XML file and removed all the normal configurations setting up blank as the default.
  • Modified the ONET.XML file so that no web parts would be placed on the site and that no lists would be created upon creation.

Now using Visual Studio .NET, I created a blank Web Project (C# example here). I used this as a temporary place to build my aspx page before deploying it to the custom site definition. To setup the project, I knew that the resulting assembly would need to be deployed to the GAC.

  • Therefore, I added an AssemblyInfo.cs file that would point to my key file.
  • I added a CreatePage.aspx ASP.NET web page
  • I added a reference to Windows.SharePoint which contains the WebPartPages namespace.
  • I changed the code-behind file to derive from WebPartPage instead of the ASP.NET Page base class:
    public class CreateSite : Microsoft.SharePoint.WebPartPages.WebPartPage
  • I modified the Page directive in the aspx file to reference the strong name of the assembly since it would be deployed to the GAC:
    Inherits="Sample.WebPartPages.CreateSite, Sample.WebPartPages,Version=1.0.0.0,Culture=neutral,PublicKeyToken=70d65f28d132a2b2"
  • Since I wanted my entered data from the form to be stored in an existing list (called “Requested Sites”) in my IT team site, I added a reference to the WSS Lists web service.

Here is a screen shot of what my aspx file looked like. Mainly it is just two panels, one that has the form and the other a confirmation message. In terms of data, I was simply asking the faculty member for their department, class number, and any comments.

Custom WebPartPage

 

So when the faculty member fills out this form, I want to create a new item in a list called “Requested Sites”. Here is the code snippet I have for this. No rocket science here. The difficulty is creating the Xml string that is a parameter to the web service call:

 

XmlDocument xml = new XmlDocument();

//create root

XmlElement batch = xml.CreateElement("Batch");

batch.SetAttribute("OnError","Return");

xml.AppendChild(batch);

//create method element

XmlElement method = xml.CreateElement("Method");

method.SetAttribute("ID", "1");

method.SetAttribute("Cmd", "New");

batch.AppendChild(method);

//create fields

XmlElement field1 = xml.CreateElement("Field");

field1.SetAttribute("Name", "ID");

field1.AppendChild(xml.CreateTextNode("New"));

method.AppendChild(field1);

XmlElement field2 = xml.CreateElement("Field");

field2.SetAttribute("Name", "Title");

field2.AppendChild(xml.CreateTextNode("Request " + DateTime.Now.ToShortDateString()));

method.AppendChild(field2);

XmlElement field3 = xml.CreateElement("Field");

field3.SetAttribute("Name", "Department");

field3.AppendChild(xml.CreateTextNode(this.ddlDepartment.SelectedValue));

method.AppendChild(field3);

XmlElement field4 = xml.CreateElement("Field");

field4.SetAttribute("Name", "ClassNumber");

field4.AppendChild(xml.CreateTextNode(this.txtCourseNum.Text.ToString()));

method.AppendChild(field4);

XmlElement field5 = xml.CreateElement("Field");

field5.SetAttribute("Name", "Comments");

field5.AppendChild(xml.CreateTextNode(this.txtComments.Text.ToString()));

method.AppendChild(field5);

ListService.Lists listService = new ListService.Lists();

listService.Url="https://wss1.sample.microsoft.com/it/_vti_bin/lists.asmx";

listService.Credentials = System.Net.CredentialCache.DefaultCredentials;

listService.UpdateListItems("Requested Sites",batch);

 

With the custom webpartpage created, I deployed the assembly to the GAC and copied my aspx page to a Wizards folder of the MYWIZARDS site definition. With the page here, I had to modify the ONET.XML file to register it. I accomplished this through the use of modules:

<Modules>

            <Module Name="DefaultBlank"/>

            <Module Name="WebPartPopulation"/>

            <Module Name="Wizards"/>

</Modules>

<Module Name="Wizards" Url="Wizards" Path="Wizards">

            <File Url="CreateSite.aspx" Type="Ghostable" />

</Module>

Bottom line, I now could create an instance of the MYWIZARDS site definition at https://wss1.sample.microsoft.com/wizards and within this site /wizards/createsite.aspx was now my custom aspx webpartpage.

 

Processing the Request

So now when the faculty member completes the form, a new list item in the “Requested Sites” list gets created. I wanted to make my admin’s job extremely easy. So in addition to putting the web part of this list on the page, I also constructed a RowConsumer web part that would take a selection and actually perform the site provisioning.

Class Provision Web Part

 

To build the RowConsumer, I took the code from the SDK and only made a few changes. First, I made a change in the RenderWebPart method so that the index instead of the field name was used when displaying results. I was getting an error initially which is likely related to the column names of my list.

 

I also added a button control that was to be displayed when the user first reviews data in the web part. To make this happen, I used an override of CreateChildControl() and setup the event handler in an override of the OnLoad event handler. The first line here calls EnsureChildControls to make sure an instance of the button exists. I also used a Boolean to keep track of whether or now the Approve button had actually been clicked.

protected override void CreateChildControls()

{

      base.CreateChildControls ();

      btnApprove = new System.Web.UI.WebControls.Button();

      btnApprove.Text = "Approve";

      this.Controls.Add(btnApprove);

}

protected override void OnLoad(EventArgs e)

{

      this.EnsureChildControls();

      btnApprove.Click += new EventHandler(btnApprove_Click);

base.OnLoad (e);

}

Inside the RenderWebPart method:

if (!isApproved)

{

btnApprove.RenderControl(output);

}

else

{

output.Write("Site Provisioned!");

}

Now all of the real work happens in the approve button’s OnClick handler. Here I use a web reference to the WSS Admin web service to call CreateSite(). You will notice the first line retrieving the DataRow from cache. This was a bit of a demo hack. Due to the number of postbacks you have to store the passed DataRow from the provider so that it is available on the postback of the Approve button.

//retrieve datarow

DataRow[] selectedRow = (DataRow[]) this.Page.Cache["datarow"];

string classNumber = selectedRow[0][1].ToString();

string username = selectedRow[0][3].ToString();

AdminService.Admin admService = new AdminService.Admin();

admService.Credentials= System.Net.CredentialCache.DefaultCredentials;

admService.CreateSite(

"https://wss1.sample.microsoft.com/classes/" + classNumber,

"Class Site", "Description", 1033, "STS#0",

      "sample\\" + username,username,

"prof1@sample.microsoft.com","","");

isApproved = true;

 

Now this should be a bit nicer, like retrieving the email address from the profile database, but you get the idea. The result is that when the admin clicks the button, the result is that the team site gets created in the virtual server we wanted, under the managed path, with enforcement that the class number should be the URL, with a specific template and configuration, and with ownership configured. Needless to say, this has a big WOW factor in a demo, but I recommend running them through it before showing how you built it!