Hosting Multiple unique SharePoint Sites on a single server

Recently we upgraded to a new, faster server for our websites. Ever since WSS v3 was released to the web I’ve been running SharePoint on my home server. But now with the new server hosted in Texas, I decided to move our SharePoint site to this server.

There are a few differences between my home server and our new web server. My home server is also my domain controller but I didn’t want to setup a domain on the new web server. The home server really only needs to service our needs, but our web server needs to be able to service the needs of myself and others. Because of these two differences, I needed a way to run multiple sharepoint sites that were totally independent of each other including users. This is normally done in SharePoint using Active Directory Account Create Mode, but like I said, I didn’t want to run a domain. A new feature with WSS V3 is the ability to have forms based authentication with ASP.NET’s membership providers. This was what I needed to do.

I first found this blog entry http://weblog.vb-tech.com/nick/archive/2006/06/14/1617.aspx?harrison on how to setup forms based authentication using a SQL Server membership provider. I was able follow his directions and set this up but there were a few issues. One, these directions assume that you will only be having one membership provider per sharepoint server, which was not the case for me. I needed to have a different membership provider for each sharepoint site on this one server. Second, I had no good way of adding, editing, and deleting users.

To fix the first issue involved doing a little copy and paste action out of the machine.config file into my web.config for the SharePoint site. I needed to change the name of the membership provider and tell this SharePoint site to use my newly named membership provider as the default provider. The reason I have to have a unique name for each SharePoint site is because I need to add every membership provider for all of my sites to the central administrations web.config. My SharePoint site’s web.config membership code looks like the following:

<membership defaultProvider="TheLineberrys_Users">
	<providers>
		<add name="TheLineberrys_Users" type="System.Web.Security.SqlMembershipProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" connectionStringName="LocalSqlServer" enablePasswordRetrieval="false" enablePasswordReset="true" requiresQuestionAndAnswer="true" applicationName="TheLineberrys" requiresUniqueEmail="true" passwordFormat="Hashed" maxInvalidPasswordAttempts="5" minRequiredPasswordLength="7" minRequiredNonalphanumericCharacters="1" passwordAttemptWindow="10" passwordStrengthRegularExpression="" />
	</providers>
</membership>
<profile enabled="true" defaultProvider="TheLineberrys_Profiles">
	<providers>
		<add name="TheLineberrys_Profiles" connectionStringName="LocalSqlServer" applicationName="TheLineberrys" type="System.Web.Profile.SqlProfileProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
	</providers>
</profile>
<roleManager enabled="true" defaultProvider="TheLineberrys_Roles">
	<providers>
		<add name="TheLineberrys_Roles" connectionStringName="LocalSqlServer" applicationName="TheLineberrys" type="System.Web.Security.SqlRoleProvider, System.Web, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
	</providers>
</roleManager>

Notice that my applicationName is the unique name of my website. This allowed me to have one membership database for all of the SharePoint sites but also be able to have completely seperate users, profiles, and roles.

To fix the second issue I found this blog http://weblogs.asp.net/scottgu/archive/2005/10/18/427754.aspx with sample code. This was good enough to start out with but didn’t provide all of the features I needed. I added this code as a virtual directory under my sharepoint website called profile. I next found http://www.qualitydata.com/products/aspnet-membership/default.aspx which provided a membership manager control that seemed to fit my needs perfectly. I had a few issues getting it working in my profile virtual directory because of SharePoint’s trust level. Eventually I found out that I needed to change the web.config of this virtual directory to full trust to fix my issues. The problem with the Membership Manager is it didn’t allow end users to manage their account, it was more of an administrator tool. So I decided to use a combination of the Profile Sample from Scott Guthrie and the Membership Manager. The Membership Manager will be my profile tools for administrators only, and from the Profile Sample I will use the changepassword and recoverpassword pages for the end users to do password management, and I will also use the createnewwizard as sort of a setup script when I first create a new SharePoint site.

After getting all of this working I needed to make it look pretty. I created my own SharePoint stripped master page based on C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\simple.master. Next I setup the colors for the Membership Manager to use SharePoint css classes and also made a profile landing page that based on your permissions and whether or not you are logged in shows the appropriate links.

But this wasn’t quite good enough. The end users would have to remember to go to /profile/ to change their password. This was unacceptable.

I decided to modify some of the SharePoint files. The first file I modified was C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\CONTROLTEMPLATES\Welcome.ascx which is the file that controls the menu items in the welcome menu at the top right of the page when you are logged in. I added the following right above the logout item:

<SharePoint:MenuItemTemplate runat="server" id="ID_ChangePassword"
Text="Change Password"
Description="Change my password used to login"
MenuGroupId="200"
Sequence="250"
UseShortId="true"
ClientOnClickNavigateUrl="/profile/changepassword.aspx"
/>

This now added a Change Password link on the welcome menu of every page for every SharePoint site on the server.

I also changed
C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\login.aspx which is the file people see when they are trying to login. I added the following right above the last </asp:Content>:

<p>
<a href="/profile/recoverpassword.aspx">Forgot your password?</a>

So now I have a change password link on every page when someone is logged in and a forgot your password link on the login page.

Another issue I ran into using forms authentication is the search seemed to not be working. I then found this page http://wsssearch.com/formauthentication.html that explains how you have to add another web application that is mapped to your forms authenticated web application so the search crawler can use windows authentication to crawl the website. After following the directions on that page my search was up and going.

Viewing SharePoint Webparts in Frontpage

I’ve recently been developing a lot of web parts for SharePoint at work. According to the Microsoft best practices for web part development they suggest you implement the IDesignTimeHtmlProvider interface. This means you need to implement a function called GetDesignTimeHtml(). This allows you to define the html output to display when editing a sharepoint page in frontpage editor. A lot of web parts I have downloaded on the web don’t seem to implement this because they show up in frontpage saying “The Preview for this Web Part is not available”. I also had a hard time implementing this since the GetDesignTimeHtml expects a string of html and when using webparts you are rendering user controls lots of times. I finally found an easy way to render almost any webpart in frontpage.

 

public virtual string GetDesignTimeHtml()
{
	StringWriter sw = new StringWriter();
	HtmlTextWriter tw = new HtmlTextWriter(sw);
	try
	{
		CreateChildControls();
		RenderWebPart(tw);
	}
	catch(Exception e)
	{
		sw.Write(e.Message + e.StackTrace);
	}
	return sw.ToString();
}

 

Basically in this function you will need to call any other functions/events that normally get called in the webpart execution (such as CreateChildControls). Then using the htmltextwriter and string writer you can output the html that would be rendered. I included the try catch so I can see the errors that are displayed when trying to render a webpart in frontpage. When in production you should probably remove the try catch so the end user sees a more friendly error message.

Something I did notice was frontpage seems to run the page in a different context than the page is normally rendered. This means that when I was trying to access Page.User.Identity.Name I got an error in frontpage. So I had to use the SPControl.GetContextWeb(Context).CurrentUser.LoginName instead. I also noticed that because frontpage loads it in a different context that when loading an Itemplate from an external ascx file, I got the error saying “The virtual path” and then whatever the path is to my ascx file “maps to another application, which is not allowed”.

 

Javascript 2 Dimensional QuickSort

Here is some code I found and modified for a Javascript 2 Dimensional Quicksort. I was using a bubble sort but decided to change over to Quicksort considering I was going to have a huge dataset.

———————————————————

function Quicksort(vec, k){
QuicksortHelp(vec, k, 0, vec[k].length-1);
}

function QuicksortHelp(vec, k, loBound, hiBound){
if (debug)
if (!confirm(loBound + " " + hiBound))
return;
var loSwap, hiSwap, temp;
var pivot = new Array(vec.length);
// Two items to sort
if (hiBound - loBound == 1){
if (vec[k][loBound].toLowerCase() > vec[k][hiBound].toLowerCase())
swap(vec,loBound,hiBound);
return;
}
// Three or more items to sort
for (counter=0; counter < vec.length; counter++){
pivot[counter] = vec[counter][parseInt((loBound + hiBound) / 2)];
vec[counter][parseInt((loBound + hiBound) / 2)] = vec[counter][loBound];
vec[counter][loBound] = pivot[counter];
}
loSwap = loBound + 1;
hiSwap = hiBound;

mycount = 0
do {
if (debug)
if (!confirm(loSwap + " YEA " + hiSwap))
return;
// Find the right loSwap
while (loSwap <= hiSwap && vec[k][loSwap].toLowerCase() <= pivot[k].toLowerCase()){
loSwap++;
}

// Find the right hiSwap
while (vec[k][hiSwap].toLowerCase() > pivot[k].toLowerCase()){
hiSwap--;
}

// Swap values if loSwap is less than hiSwap
if (loSwap < hiSwap)
swap(vec,loSwap,hiSwap);
mycount++;
} while (loSwap < hiSwap);

for (counter=0; counter < vec.length; counter++){
vec[counter][loBound] = vec[counter][hiSwap];
vec[counter][hiSwap] = pivot[counter];
}

if (debug)
if (!confirm(loBound + " < " + (hiSwap-1)))
return;
// Recursively call function... the beauty of quicksort

// 2 or more items in first section 
if (loBound < hiSwap - 1)
QuicksortHelp(vec, k, loBound, hiSwap - 1);

if (debug)
if (!confirm((hiSwap+1) + " < " + hiBound))
return;
// 2 or more items in second section
if (hiSwap + 1 < hiBound)
QuicksortHelp(vec, k, hiSwap + 1, hiBound);
}

function swap(arr,p1,p2){
for (k=0; k temp = arr[k][p1];
arr[k][p1] = arr[k][p2];
arr[k][p2] = temp;
}
}

———————————————————

I also added the following code to sort blank entries last. You just need to run this before you call quicksort.

for (i=0; i < vec[k].length; i++){
if (vec[k][i].length <= 0){
vec[k][i] = "" + String.fromCharCode(127);
}
}

And run this after you call quicksort.

for (i=0; i < vec[k].length; i++){
if (vec[k][i] == ("" + String.fromCharCode(127))){
vec[k][i] = "";
}
}