Thursday, October 1, 2009

Using InfoPath Task Forms in Visual Studio 2008 Workflows for SharePoint

I was recently creating a SharePoint workflow within Visual Studio 2008 and needed to use InfoPath forms as task forms instead of the default task forms that you get as default. Well, I found a great article explaining how to do this in great detail - which saved me quite a bit of time figuring this out myself. So for others looking to do the same, I would highly recommend this blog post by Fodi Dervidis here.

Passed the MOSS Configuration Exam 70-630

I just passed the MOSS Configuration exam 70-630 yesterday. I had heard it was an easier exam than the 70-631 and I found out why. I didn't spend too much time studying for it and got a perfect score in most of the sections. Looking forward to SharePoint 2010 now.

Thursday, September 17, 2009

SharePoint Server Disable LoopbackCheck

This is a scenario I have come across a few times now, especially working on Windows Server 2008 boxes. It seems that this is a security fix that Microsoft introduced since Windows Server 2003 SP1. Basically what it fixes (breaks in SharePoint) is explained below.

1. You create a Web application in SharePoint that uses a host header that you intend to make available to end users. Lets say http(s)://www.company.com.

2. You can browse to this URL file from the other machines or over the Web.

3. You remote into the server to make some changes - and you decide to open a browser and type http://www.company.com in the address. You usually get asked to login multiple times and after three tries it will usually show you a blank screen with a 401.1 access denied error. Note that this only happens when you are trying to access the website on the same server as you are logged into.

Enter the DisableLoopBackCheck setting in the Registry. Spencer Harbar wrote up a great article about the same that I ran across as I was writing this post, so I will just pass you on to his splendid explanation and resolution of this problem here.

Tuesday, September 15, 2009

Setting the default Active Directory in SharePoint People Picker

If you need to specify a default Active Directory that a People Picker control should find users from - and not to use any other trusted domains in its search, here is a nifty stsadm command for the following.

"C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\BIN\stsadm" -o setsiteuseraccountdirectorypath -path "DC=www,DC=company,DC=com" -url https://[your site url here]

Exposing SharePoint List Data using Data View Web Part

Recently I had the need to create a new view when a user clicked on a list item. This view was formatted like a HTML page, and looked nothing like the default DispForm.aspx that SharePoint shows you by default. Well, I ended up implementing a nifty solution that my colleague at work had used before.

Instead of replacing or customizing DispForm.aspx for the list (which you need to be really careful about), I actually created a new WebPart page in a document library on that site. I cracked open SharePoint designer and threw a Data View/Form Web Part (or the Swiss Army knife as it is sometimes called) and added a URL Querystring parameter to provide me with the ID of the item I was showing. Then I customized the design of the page inside the XSL to show me the view that I wanted.

The last part was to wire up the click event on the list to this new page. I browsed to DispForm.aspx and opened it up in edit mode. Some of you might wonder how is it possible to edit an administrative page? The answer lies in the querystring parameters you provide it in the URL. If you append a querystring ?PageView=Shared&ToolPaneView=2 to the page, then it will open it up in Edit view. From here on, I just hid the list view Web Part and added a content editor Web Part that contained some javascript to redirect to the page in my document library that had the Data View Web Part with the formatting I needed.

Thursday, September 3, 2009

Microsoft case study on RE/MAX Extranet

Please check out the recently released Microsoft case study on RE/MAX which is a project I was involved in from the beginning and worked on for a little less than a year. It was a great suceess and we used a lot of cool features in MOSS to build the extranet, meet the business needs and exceed expectations. Here is the case study.

SharePoint Guidance v2 released

The second version of the SharePoint guidance was just released recently. Be sure to have a look and to incorporate the guidance and the architectural decisions that can help you in your SharePoint implementation projects. Here is the MSDN site. Here is the project site on codeplex. Enjoy!

Thursday, August 20, 2009

Passed the Windows SharePoint Services Configuration 70-631 exam

So I just passed the Windows SharePoint Configuration exam 70-631 yesterday. There were a few tough questions about NLBs and ISA server, but overall I think it was a pretty easy exam, especially because the some of the multiple choices were way off.

Friday, July 24, 2009

SharePoint User Group Presentation Slide Deck July 2009

I wanted to take the time to thank everyone who attended the User Group Presentation last week where we presented last week. Here is the slide deck as promised. Enjoy!

Friday, June 12, 2009

Visual Studio Extensions: How to Change Base Type of Content Type

Recently I came across an interesting challenge. I needed to create some custom fields on my custom Page Layout for a client. I needed to add about 3 fields in addition to the fields in the existing page layouts (MetsKeywords, MetaDescription and a PageContent2 which were multi-line, multi-line and HTML fields respectively).

I went ahead and tried to create a custom content type in the VSEWSS and it popped open the Base Type dialog box. I wanted the Article Page to be my base type, but I didn't find it in the dropdown. I figured it would be easy to change later and so I picked Item as my base.

The first piece that was puzzling is that even though I picked a base as part of creating the content type, there was no reference to that base in the xml file. There is an attribute in the content type xml called BaseType which I figured I could set to the ctype ID of the actual base type I wanted (you can go to your SharePoint site, go to site settings, content types, and click on the content type you wish to inherit from - you will notice the ctype id in the URL. I copied and pasted it in the BaseType attribute and deployed and it still did not work.

After some digging around, I realized that the BaseType does not define the parent type. The parent type ID has to be the first part of the ID of the current content type. What I do now is copy the parent ID into the ID of the content type - and add 0D after that to specify the association.

Friday, May 8, 2009

SharePoint SP2 is out

I don't know if any of you know this, but the SP2 for SharePoint was released on April 28.

The following text is from the Microsoft blog post

Benefits

Customers can be benefited from the following enhancements with Service Pack 2.

  • Performance and Availability Improvements

Service Pack 2 includes many fixes and enhancements designed to improve performance, availability, and stability in your server farms, including:

    • New Timer job automatically rebuilds content database index to improve database performance.
    • When a content database is marked as read-only, the user interface will be modified so users cannot perform tasks that require writing to the database.
    • Performance enhancement across nearly all the components. 
  • Improved Interoperability

Service Pack 2 continues to improve SharePoint interoperability with other products and platforms.

    • Broader support of browsers 
      Internet Explorer 8 is added into Level 1 browser support. 
      FireFox 3.0 is added into Level 2 browser support. (Firefox 2.0 is no longer supported by Mozilla)
    • Provide improved client integration user experience with Form Based Authentication. Now the client application can store user credentials instead of asking for them every time. For more technical details please refer to the updated articles on TechNet. 
      Configure forms-based authentication (Office SharePoint Server 
      http://technet.microsoft.com/en-us/library/cc262201.aspx 
      Configure forms-based authentication (Windows SharePoint Services) 
      http://technet.microsoft.com/en-us/library/cc288043.aspx 
  • Getting Ready for SharePoint Server 2010

A new preupgradecheck operation is added to stsadm tool. It can be used to scan your server farm to establish whether it is ready for upgrade to SharePoint Products and Technologies "14". It identifies issues that could present obstacles to the upgrade process. It checks for several SharePoint Products and Technologies "14" system requirements, including the presence of Microsoft® Windows Server® 2008 and a 64-bit hardware, and provides feedback and best practice recommendations for your current environment, together with information on how to resolve any issues that the tool discovers. 



Along with all the improvements, the preupgrade check is going to be handy for analyzing the  upgrade path to Office 14. I actually went ahead and installed the Service Pack 2 on a SharePoint server which only had SP1 installed on there and no updates after that. The upgrade process went smoothly with no hiccups. The one thing to note is that SP2 is available as a separate download for both WSS and MOSS. The SharePoint Products and Technologies Configuration wizard will pop up as soon as you finish installing the WSS SP2. It will save time to exit out of that (if you have MOSS installed), then install the SP2 for MOSS and then run the wizard. This wizard modifies database schema, so it saves time to run it once for both. 

SP2 can be installed on any build before before Fenruary 2009, which means that you could install it on a SharePoint server that has no service packs installed yet, just SP1, or the infrastructure update and builds after that but before February 2009.

This means that the newly released April "uber" package that Stefan talks about here is not included in SP2. The "uber" package will need to be installed after SP2.

Just to be safe, please try installing SP2 on a test/stage environment before unleashing it on production to make sure it will work for you :).


Update: I installed the SP2 as mentioned above, but ran into an issue right after where I could not authenticate to SharePoint using IE7 or SP Designer. I tried getting all the latest updates but that did not help either. I tried authenticating with Firefox and it authenticated just fine. So in case any of you come across this authentication with IE issue, here is what I found after some looking around.

  1. Click Start, click Run, type regedit, and then click OK.
  2. In Registry Editor, locate and then click the following registry key:
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa
  3. Right-click Lsa, point to New, and then click DWORD Value.
  4. Type DisableLoopbackCheck, and then press ENTER.
  5. Right-click DisableLoopbackCheck, and then click Modify.
  6. In the Value data box, type 1, and then click OK.
  7. Quit Registry Editor, and then restart your computer.

I tried this out and it worked wonderfully.

Thursday, May 7, 2009

Script to Create Profile Properties in SharePoint Profile Store

Recently I was working on a large enterprise project that made extensive use of the user profile store in SharePoint. To meet the business requirements we needed a lot of new properties to be created in the profile store. While that may initially look easy to just open up the profile section in the SSP and start creating properties by hand - you need to take into account different developer VPCs and the dev/test/stage/prod environments (and suddenly its not so easy). There was also a need to map these properties to either AD or the BDC connection. That is where the data was being pulled from.

So here is a handy little script I wrote to create all the profile properties you need (its configurable via the application file) and also to map these properties to the appropriate connector (if you are pulling data from AD or BDC).

I wrote this in a hurry and this is not production quality code, so take it as it is :).



using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Office.Server;
using Microsoft.Office.Server.Administration;
using Microsoft.Office.Server.UserProfiles;
using Microsoft.SharePoint;
using System.Web;
using System.Configuration;
using System.Collections;
using System.Collections.Specialized;
using Microsoft.SharePoint.Portal;
using Microsoft.SharePoint.Portal.Topology;
using Microsoft.SharePoint.Administration;


namespace company.Mt.Utilities.ProfileProperties
{
class Program
{


static void Main(string[] args)
{
if (args.Length != 1)
{
Usage();
return;
}

switch (args[0])
{
case "CreateProfileAttributes":
CreateProfileAttributes();
MapExistingAttributes();
break;

case "MapExistingAttributes":
//MapExistingAttributes();
break;

case "DeleteOtherAttributes":
DeleteOtherAttributes();
break;

//case "DeleteUserProfiles":
// ProfileInstanceManagement.DeleteUserProfiles();
// break;

default:
Usage();
break;
}
}


///
/// Display user information
///

private static void Usage()
{
Console.WriteLine("Usage: Program [choice] where choice = CreateProfileAttributes or MapExistingAttributes or DeleteOtherAttributes");
Console.WriteLine("Example --> Program CreateProfileAttributes");
}


///
/// Delete other attributes
///

private static void DeleteOtherAttributes()
{
using (SPSite site = new SPSite(ConfigurationManager.AppSettings["server"])) //("http://localhost"))
{
ServerContext context = ServerContext.GetContext(site);
UserProfileManager profileManager = new UserProfileManager(context);
UserProfileConfigManager mgr = new UserProfileConfigManager(context);


//Get the properties
PropertyCollection pc = profileManager.PropertiesWithSection;

//Property companySection = pc.GetPropertyByName(ConfigurationManager.AppSettings["CustomPropertySectionName"]);
//if (companySection == null)
//{
// companySection = pc.Create(true);
// companySection.Name = ConfigurationManager.AppSettings["CustomPropertySectionName"];
// companySection.DisplayName = ConfigurationManager.AppSettings["CustomPropertySectionValue"];
//}

int order = 1;
NameValueCollection coll = (NameValueCollection)ConfigurationManager.GetSection("company/NewProfileProperties");

string keyValue = null;

foreach (String key in coll.AllKeys)
{
try
{
pc.RemovePropertyByName(key);
}
catch (Exception ex)
{
string s = ex.Message;
}
}
}


using (SPSite site = new SPSite(ConfigurationManager.AppSettings["server"])) //("http://localhost"))
{
ServerContext context = ServerContext.GetContext(site);
UserProfileManager profileManager = new UserProfileManager(context);
UserProfileConfigManager mgr = new UserProfileConfigManager(context);

PropertyCollection pc = profileManager.Properties;

NameValueCollection coll = (NameValueCollection)ConfigurationManager.GetSection("company/ExistingProfilePropertyUpdates");

string keyValue = null;

foreach (String key in coll.AllKeys)
{
try
{


keyValue = coll[key];
String[] details = keyValue.Split(new char[] { ',' });


Property property = pc.GetPropertyByName(key);
property.DisplayName = details[0];

DataSource ds = mgr.GetDataSource();
PropertyMapCollection pmc = ds.PropertyMapping;

string tomap = details[1].Trim();
string connnection = details[2].Trim();

try
{
pmc.Remove(key);
}
catch (Exception exe) { }

ArrayList invmap = pmc.VerifyMapping(false);

property.Commit();
}
catch (System.Exception e2)
{
Console.WriteLine(e2.Message + " - " + key);
}
}
}

}

///
/// Change mappings for existing attributes
///

private static void MapExistingAttributes()
{
using (SPSite site = new SPSite(ConfigurationManager.AppSettings["server"])) //("http://localhost"))
{
ServerContext context = ServerContext.GetContext(site);
UserProfileManager profileManager = new UserProfileManager(context);
UserProfileConfigManager mgr = new UserProfileConfigManager(context);

PropertyCollection pc = profileManager.Properties;

NameValueCollection coll = (NameValueCollection)ConfigurationManager.GetSection("company/ExistingProfilePropertyUpdates");

string keyValue = null;

foreach (String key in coll.AllKeys)
{
try
{
keyValue = coll[key];
String[] details = keyValue.Split(new char[] { ',' });


Property property = pc.GetPropertyByName(key);
property.DisplayName = details[0];

DataSource ds = mgr.GetDataSource();
PropertyMapCollection pmc = ds.PropertyMapping;

string tomap = details[1].Trim();
string connnection = details[2].Trim();

try
{
pmc.Remove(key);
}
catch (Exception exe) { }

if ((!string.IsNullOrEmpty(tomap)) && (!(string.IsNullOrEmpty(connnection))))
pmc.Add(key, tomap, connnection);

//ArrayList invmap = pmc.VerifyMapping(false);

property.Commit();
}
catch (System.Exception e2)
{
Console.WriteLine(e2.Message + " - " + key);
}
}
}

}

///
/// Create the profile attributes
///

private static void CreateProfileAttributes()
{

//
using (SPSite site = new SPSite(ConfigurationManager.AppSettings["server"])) //("http://localhost"))
{
ServerContext context = ServerContext.GetContext(site);
UserProfileManager profileManager = new UserProfileManager(context);
UserProfileConfigManager mgr = new UserProfileConfigManager(context);


//Get the properties
PropertyCollection pc = profileManager.PropertiesWithSection;

//Property companySection = pc.GetPropertyByName(ConfigurationManager.AppSettings["CustomPropertySectionName"]);
//if (companySection == null)
//{
// companySection = pc.Create(true);
// companySection.Name = ConfigurationManager.AppSettings["CustomPropertySectionName"];
// companySection.DisplayName = ConfigurationManager.AppSettings["CustomPropertySectionValue"];
//}

int order = 1;
NameValueCollection coll = (NameValueCollection)ConfigurationManager.GetSection("company/NewProfileProperties");

string keyValue = null;

foreach (String key in coll.AllKeys)
{
try
{
keyValue = coll[key];
String[] details = keyValue.Split(new char[] { ',' });

Property p = pc.Create(false);
p.Name = key;
p.DisplayName = details[0].Trim();
//p.Type = details[1].Trim().ToLower();

//sample to get a property type "URL"
PropertyDataTypeCollection pdtc = mgr.GetPropertyDataTypes();
PropertyDataType ptype = null;
IEnumerator enumType = pdtc.GetEnumerator();
while (enumType.MoveNext())
{
ptype = (PropertyDataType)enumType.Current;
if (ptype.Name.ToLower().Equals(details[1].Trim().ToLower())) break;
}

p.Type = ptype.Name;

if (details[1].Trim().ToLower() == "string")
p.Length = Convert.ToInt32(details[2].Trim());

p.PrivacyPolicy = PrivacyPolicy.OptIn;
p.DefaultPrivacy = Privacy.Public;
if (details[3].Trim() == "true")
p.IsSearchable = true;
else
p.IsSearchable = false;

p.IsVisibleOnEditor = false;
p.IsVisibleOnViewer = true;
pc.Add(p);
pc.SetDisplayOrderByPropertyName(p.Name, order++);
pc.CommitDisplayOrder();


DataSource ds = mgr.GetDataSource();
PropertyMapCollection pmc = ds.PropertyMapping;


string tomap = details[4].Trim();
string connnection = details[5].Trim();

if ((!string.IsNullOrEmpty(tomap)) && (!(string.IsNullOrEmpty(connnection))))
pmc.Add(key, tomap, connnection);

//ArrayList invmap = pmc.VerifyMapping(false);
}
catch (DuplicateEntryException e)
{
Console.WriteLine(e.Message + " - " + key);
}
catch (System.Exception e2)
{
Console.WriteLine(e2.Message + " - " + key);
}
}


//get arraylist of invalid mappings
//ArrayList invmap = pmc.VerifyMapping(false);
//remove current mapping if invalid
//foreach (PropertyMap pm in invmap)
//{
// if (pm.PropName == p.Name)
// {
// pmc.Remove(pm.PropName);
// }
//}

}
}
}
}

Here are the corresponding entries from the web.config.


<configuration>
<configsections>
<sectiongroup name="Company">
<section name="NewProfileProperties" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
<section name="ExistingProfilePropertyUpdates" type="System.Configuration.NameValueSectionHandler, System, Version=1.0.5000.0, Culture=neutral, PublicKeyToken=b77a5c561934e089">
</section></section></sectiongroup>
</configsections>
<company>
<newprofileproperties>
<!-- key=name, value="Display name, type, length, true if indexed, mapping to AD/BDC"/> -->

<add key="NamePrefix" value="Name Prefix,String,20,false,NamePrefix,BDC_Connection_Name"></add>
<add key="NameTitle" value="Name Title,String,40,false,NameTitle,BDC_Connection_Name"></add>
<add key="CompanyEmail" value="Company Email,Email,,true,userPrincipalName,ad_Connection_name"></add>

</newprofileproperties></company></configuration>
......
......

MOSS custom navigation provider for left nav

As promised, here is the code for the custom left navigation provider in MOSS. It uses a similar concept as the custom top navigation provider I detailed in this post, but there are some differences. Unlike a top or global navigation which is fairly consistent across your brand, the left navigation can change from site across site collections. It can even be different in different Webs. So based on that consideration, this solution handles those cases. Again, this navigation reads from a SharePoint list on a site. The navigation can read from a list in the root site of a site collection, the current site, or a site below the root site (this was a specific requirement) - all this is in a case statement so pretty easy to change.

Again, I have used this before successfully for a busy site. There are various settings in the Web.config that allow you to customize as per your needs.

This is demonstrated in the picture below. Only the Quick view is changeable on a site per site basis. The Quick Links section is consistent across the brand.





Again, the navigation is being read from a list in SharePoint, such as this. This doesn't have the same data, but should get the point across anyway.




#region Code Comment Header
/*******************************************************************************************
*
*
* $History: CustomLeftNavProvider.cs $
*
* *****************************************************************************************/
#endregion
using System;
using System.Configuration;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.Caching;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Security;
using Microsoft.SharePoint.Publishing;
using Microsoft.SharePoint.Publishing.Navigation;
using Microsoft.SharePoint.Administration;
using CompanyXXX.ExceptionManagement;
using System.Security.Permissions;
using System.Security;

namespace CompanyXXX.Dept.MOSS.Utilities.Navigation.Providers
{
//Assign the neccessary security permissions
[AspNetHostingPermissionAttribute(SecurityAction.LinkDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[SharePointPermissionAttribute(SecurityAction.LinkDemand, ObjectModel = true)]
[AspNetHostingPermissionAttribute(SecurityAction.InheritanceDemand, Level = AspNetHostingPermissionLevel.Minimal)]
[SharePointPermissionAttribute(SecurityAction.InheritanceDemand, ObjectModel = true)]
public class CustomLeftNavProvider : PortalSiteMapProvider
{
//Create the in memory objects for storage and fast retreival
protected SiteMapNodeCollection siteMapNodeColl;
protected ArrayList childParentRelationship;
protected ArrayList topLevelNodes;
//protected SPSite sourceListSite;
//protected SPWeb sourceListWeb;
//protected bool needDisposing = false;

///
/// Override the initialize method of the superclass
///

///
///
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// Verify that config isn't null
if (config == null)
throw new ArgumentNullException("config is null");

// Assign the provider a default name if it doesn't have one
if (String.IsNullOrEmpty(name))
name = "CustomLeftNavProvider";

// Add a default "description" attribute to config if the
// attribute doesn’t exist or is empty
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description", "CompanyXXX custom current left navigation provider");
}

base.Initialize(name, config);
}


///
/// This method will be called for all nodes and subnodes that can have children under them. For eg, NodeTypes.AuthoringLink type node
/// cannot have child nodes.
///

/// The node to find child nodes for
/// The SiteMapNodeCollection which contains the children of the child nodes
public override SiteMapNodeCollection GetChildNodes(System.Web.SiteMapNode node)
{
return ComposeNodes(node);
}

///
/// Find the web and the list from where to read navigation
///

///
public void FindWeb(ref SPWeb sourceListWeb, ref SPSite sourceListSite, ref bool needDisposing)
{
//string currentNavList = ConfigurationManager.AppSettings["CurrentLeftNavigationListName"];
//inheritNav = true;

string navListWeb = String.Empty;

try
{
//Get the web where the current list is created
navListWeb = ConfigurationManager.AppSettings["LeftNavigationListWeb"];

//if the list is at the root site of the collection, the current web or even a different
//site - this is specified in the config file
switch (navListWeb) {
case "ROOT":
sourceListWeb = SPContext.Current.Site.RootWeb;
//leftNavList = sourceListWeb.Lists[currentNavList];
break;

case ".":
sourceListWeb = SPContext.Current.Web;
//leftNavList = sourceListWeb.Lists[currentNavList];
//inheritNav = false;
break;

case "LEVEL2":
//The list will be located one level below the top level
SPWeb cmsweb = SPContext.Current.Web;
SPWeb holder = null;
SPWeb rootWeb = SPContext.Current.Site.RootWeb;

//if current web is at second level or at root, pick the respective lists from there
if (cmsweb.ID == rootWeb.ID || cmsweb.ParentWeb.ID == rootWeb.ID)
sourceListWeb = cmsweb;
else
{
//else traverse up to the second level
while (cmsweb.ID != rootWeb.ID)
{
holder = cmsweb;
cmsweb = cmsweb.ParentWeb;
}
sourceListWeb = holder;
}
break;

default:
//instantiate sites and lists.
sourceListSite = new SPSite(navListWeb);
sourceListWeb = sourceListSite.OpenWeb();
//leftNavList = sourceListWeb.Lists[currentNavList];
//inheritNav = false;
needDisposing = true;
break;
}
}
catch (Exception ex)
{
System.Collections.Specialized.NameValueCollection exc =
new System.Collections.Specialized.NameValueCollection();

exc.Add("Method", "FindWeb");

if (!String.IsNullOrEmpty(navListWeb))
exc.Add("navListWebFromAppSetting", navListWeb);
else
exc.Add("navListWebFromAppSetting", "null");
if (sourceListSite != null)
exc.Add("sourceListSite", sourceListSite.Url);
else
exc.Add("sourceListSite", "null");
if (sourceListWeb != null)
exc.Add("sourceListWeb", sourceListWeb.Url);
else
exc.Add("sourceListWeb", "null");

throw ex;
}
}

///
/// Compose nodes when the method is called. At a minimum, this method gets called with the root node of every
/// site collection. We must attach the top level nodes to the root node for this method to get called for those
/// nodes as well.
///

///
///
public virtual SiteMapNodeCollection ComposeNodes(System.Web.SiteMapNode node)
{
//Create the SiteCollection to return
SiteMapNodeCollection children = new SiteMapNodeCollection();
SortedList orderedNodes = new SortedList();

SPWeb sourceListWeb = null;
SPSite sourceListSite = null;
SPList leftNavList = null;
bool cacheLeftNav = false;
bool needDisposing = false;
string currentNavList = String.Empty;

try
{
FindWeb(ref sourceListWeb, ref sourceListSite, ref needDisposing);

string cacheNav = ConfigurationManager.AppSettings["CacheLeftNavigation"];
if (String.Compare(cacheNav, "true", true) == 1)
cacheLeftNav = true;

//Account for the PortalWebSiteMapNode
PortalWebSiteMapNode prtlWebSiteMapNode = null;
int counter = 100;

currentNavList = ConfigurationManager.AppSettings["CurrentLeftNavigationListName"];

//If a rootnode, then add the top level children which are the same for every site collection
if (node == node.RootNode)
{
//Serve it from cache if possible
if (cacheLeftNav)
{
object topNodes = HttpRuntime.Cache["LeftNavTopNodes" + sourceListWeb.ID.ToString()];
if (topNodes != null && topNodes is SiteMapNodeCollection)
return ((SiteMapNodeCollection)topNodes);
}

//else find the list
leftNavList = sourceListWeb.Lists[currentNavList];

//Create the query to browse the list
SPQuery qry = new SPQuery();

SPFolder startFolder = null;

//Get the folder to start from
startFolder = leftNavList.RootFolder.SubFolders[ConfigurationManager.AppSettings["NavigationListStartFolderName"]];

//Get the query for this folder
qry.Folder = startFolder;

//Get all the items under the start folder using the query
SPListItemCollection ic = sourceListWeb.Lists[startFolder.ParentListId].GetItems(qry);

//Get the PortalWebSiteMapNode
if (node != null && node is PortalWebSiteMapNode)
{
prtlWebSiteMapNode = node as PortalWebSiteMapNode;

//Construct the nodes
foreach (SPListItem subitem in ic)
{
//Change the nodeTypes to Authored link for leaf nodes so that the GetChildNodes method is not called for those nodes.
NodeTypes ntypes = NodeTypes.Default;
if (subitem.Folder == null)
ntypes = NodeTypes.AuthoredLink;

//Create a PortalSiteMapNode
PortalSiteMapNode psmn = new PortalSiteMapNode(prtlWebSiteMapNode, subitem.ID.ToString(), ntypes,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlLink"]), subitem.Title,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlDescription"]));

//Order the nodes
try
{
int order = Convert.ToInt32(subitem.GetFormattedValue(ConfigurationManager.AppSettings["ItemOrder"]));
orderedNodes.Add(order, psmn);
}
catch (Exception ex)
{
orderedNodes.Add(counter++, psmn);
}
}

//Copy nodes in the right order
foreach (object portalSiteMapNode in orderedNodes.Values)
{
children.Add(portalSiteMapNode as PortalSiteMapNode);
}

//Add them to the cache
if (cacheLeftNav)
HttpRuntime.Cache["LeftNavTopNodes" + sourceListWeb.ID.ToString()] = children;
}
}
else
//Else this is a subnode, get only the children of that subnode
{
string nodeKey = node.Key;

if (cacheLeftNav)
{
//Get the children for this nodeKey from cache if they exist there
object subNodes = HttpRuntime.Cache["LeftNavChildNodes" + sourceListWeb.ID.ToString() + nodeKey];
if (subNodes != null && subNodes is SiteMapNodeCollection)
return ((SiteMapNodeCollection)subNodes);
}

leftNavList = sourceListWeb.Lists[currentNavList];

//Create the query to browse the list
SPQuery qry = new SPQuery();

//Convert the nodeKey into an integer
int ID = Convert.ToInt32(nodeKey);

//Find the current item
SPListItem item = leftNavList.GetItemById(ID);

//Find the item that and get its subitems
if (item != null && item.Folder != null)
{
SPFolder curFolder = item.Folder;

qry.Folder = curFolder;

//Get all the items in the current folder using the query
SPListItemCollection ic = sourceListWeb.Lists[curFolder.ParentListId].GetItems(qry);

//To get the PortalWebSiteMapNode, ask the root node
if (node.RootNode != null && node.RootNode is PortalWebSiteMapNode)
{
//Cast node to PortalWebSiteMapNode
prtlWebSiteMapNode = node.RootNode as PortalWebSiteMapNode;

foreach (SPListItem subitem in ic)
{
//Change the nodeTypes to Authored link for leaf nodes so that the GetChildNodes method is not called
//for those nodes.
NodeTypes ntypes = NodeTypes.AuthoredLink;
if (subitem.Folder != null)
ntypes = NodeTypes.Default;

//Create a PortalSiteMapNode
PortalSiteMapNode psmn = new PortalSiteMapNode(prtlWebSiteMapNode, subitem.ID.ToString(),
ntypes, subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlLink"]), subitem.Title,
subitem.GetFormattedValue(ConfigurationManager.AppSettings["UrlDescription"]));


//Order the nodes
try
{
int order = Convert.ToInt32(subitem.GetFormattedValue(ConfigurationManager.AppSettings["ItemOrder"]));
orderedNodes.Add(order, psmn);
}
catch (Exception ex)
{
orderedNodes.Add(counter++, psmn);
}
}

//Copy nodes in the right order
foreach (object portalSiteMapNode in orderedNodes.Values)
{
children.Add(portalSiteMapNode as PortalSiteMapNode);
}

//Add them to the cache
if (cacheLeftNav)
{
HttpRuntime.Cache["LeftNavChildNodes" + sourceListWeb.ID.ToString() + nodeKey] = children;
}
}
}
}
}
catch (Exception ex)
{
System.Collections.Specialized.NameValueCollection exc =
new System.Collections.Specialized.NameValueCollection();

exc.Add("Method", "ComposeNodes");

if (!String.IsNullOrEmpty(currentNavList))
exc.Add("CurrentLeftNavigationListNameFromAppSettings", currentNavList);
else
exc.Add("CurrentLeftNavigationListNameFromAppSettings", "null");

if (sourceListSite != null)
exc.Add("sourceListSite", sourceListSite.Url);
else
exc.Add("sourceListSite", "null");

if (sourceListWeb != null)
exc.Add("sourceListWeb", sourceListWeb.Url);
else
exc.Add("sourceListWeb", "null");

ExceptionManager.Publish(ex, exc);

//Do not dispose of the web or site referenced from SPContext

//Test that finally does get called even though a return statement is executed here
//Return empty collection
return new SiteMapNodeCollection();
}
finally
{
if (needDisposing)
{
if (sourceListWeb != null)
sourceListWeb = null;
if (sourceListSite != null)
sourceListSite = null;
}
}
return children;
}
}
}


Enjoy!!

Friday, April 10, 2009

Import user profiles using Active Directory / Business Data Catalog

It has been a while since I blogged, I was on a large enterprise project and was pretty busy until recently. I concluded the project at a client where we had a lot of user information in a legacy system that used SQL Server as the backend. We were building the new site on MOSS 2007 and the user profiles were needed in the SharePoint application - for searching and surfacing these user profiles for over 120,000 users. We were also implementing Active Directory as part of this release so we needed to bring all the user information into AD for authentication to the new MOSS site.

To implement this complex requirement, we broke the challenge into 3 tasks.

1. Moving the user accounts from the legacy database to AD to enable user authentication against AD.

Then we had to pull the same information into the SharePoint profile store. Here is how we broke this up.

2. We moved the user account information from AD to the SharePoint profiles using the built in SharePoint connector to AD. Though the user profile in the legacy system had a lot of fields, we only brought over a handful of fields to AD to minimize the impact to the Active Directory. 

SharePoint 2007 profiles have a concept of master and non-master connections. AD or a LDAP source is a master connection. BDC on the contrary, is a non-master connection. What this means that the BDC cannot be used to create a new profile record in the profile store. Only AD or a LDAP connection can do that. BDC can only supplement user information in the SharePoint profile store.

3. We supplemented the rest of the information from the custom legacy store to the SharePoint profile store using the Business Data Catalog in SharePoint. This allowed us to bring 50-60 additional fields over directly into the SharePoint profile store without needing to move them all to AD and bring them from there.





I was getting ready to write many more pages about the details of this solution, but I just noticed that my friend Todd Baginski also concluded a similar project a few weeks ago, so I am going to point you his way for all the details. You can view his post here.

The way my solution differed from Todd's is that we did not use this information in the MySites. In fact, we did not even enable MySite. The reason being that we were pulling a 120,000 users or so - and that would be nearing the 150,000 site collections per Web application upper limit recommendation on technet. We did contemplate splitting up the MySites in a couple of Web Apps, but the requirements for that user interface were also very different than what comes OOB in SharePoint so a custom page served us much better, instead of the overhead of having 120,000 site collections. Also we did not want to give users control of editing on the site.

The other notables to this piece were 
1. A handy script I wrote to create 70 profile attributes in the user profile store in SharePoint and also map those attributes to either the master connection on the BDC connection depending on the application file settings.

2. A wholly custom search implementation that used the SharePoint Enterprise Full Text search to search the user profiles and display each individual profile on the custom page that we developed and deployed to SharePoint.

I will document both these pieces shortly. Stay tuned!!