Thursday, April 29, 2010

SP 2010 - Phonetic Search Only for People Scope?

I have a custom scope I've created that I am querying against with the FullTextSqlQuery object. Recently I was asked if we could allow a phonetic search. With all the hype around this new feature in SharePoint 2010, I thought it would be very easy.

I looked, and sure enough, there is an EnablePhonetic property right on FullTextSqlQuery. I set it to true, ran my search and got back...nothing. I figured maybe it didn't like my query, which had a LIKES keyword. That wasn't it. I tried looking on the web and it seems all the mentions of phonetic search seem to be closer to press releases than coding snippets.

Finally I found this nugget on MSDN:
For FAST Search Server 2010 for SharePoint, this property is only applicable for People Search.
Well, I'm not using FAST, but I am using a custom search scope. It would appear that might be the limiting factor here. While I am searching for people, they are stored in a custom list, since they are not members of my SharePoint site. Therefore I was using a custom scope to get to them.

It would seem phonetic search is not available yet to customize in this way.

Wednesday, April 28, 2010

SP 2010 - Configure and Use a TaxonomyWebTaggingControl

If you are using the Managed Metadata Service and developing a custom web part, you may be interested in using that nice term picker that you see when you create a new list item that contains a Managed Metadata Column.

First, let's cover the bare minimum code you'll need to get the term picker to show up in your web part. First you'll want to add a reference to Microsoft.SharePoint.Taxonomy to your project. Then you'll need a Register directive in your ascx, like this:

<%@ Register Tagprefix="Taxonomy" Namespace="Microsoft.SharePoint.Taxonomy" Assembly="Microsoft.SharePoint.Taxonomy, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %> 

Now you can add the picker to your web part just like any other control.



You'll notice that I didn't set any properties on the TaxonomyWebTaggingControl. Well there are three properties you'll need to set, but we'll do it programatically. Those properties are:
  • SSPList - This property parses your string and builds a List to set the SspId property
  • GroupId - A Guid
  • TermSetList - This property parses your string and builds a List to set the TermSetId property

Now, you can set these properties declaratively if you wish, but it won't be a very portable web part, as the Guids aren't part of the import CSV format for the Managed Metadata Service, nor do any of the Taxonomy Create methods allow setting of Ids. Because of this, it's best to have your web part set these properties on the TaxonomyWebTaggingControl programatically. If you just want to see how you would go about setting these properties declaratively, scroll to the bottom of this post, as I've included it for reference.

In order to make your web part portable, you're going to need to query the Managed Metadata Service and get these values yourself. Moreover, you're going to have to do it on every load of your web part since these properties are not stored anywhere between page loads.

Note: If you only set these properties on page load, this will work as long as there are no scenarios where you need to redraw the control again, such as having a custom server validator. In that instance, your picker would work before the postback, but the look-ahead feature would be broken after the postback.

In order to make loading our properties a little easier, here is an extension method for TaxonomyWebTaggingControl that allows you to quickly set the properties. The path part of this was inspired by PHolpar. Also, remember that extension methods must be defined in static classes.

public static bool Configure(this TaxonomyWebTaggingControl twtc, string termSetPath)
{
 bool configured = false;

 TaxonomySession session = new TaxonomySession(SPContext.Current.Site);

 string[] parts = termSetPath.Split(new char[] { '/' }, StringSplitOptions.RemoveEmptyEntries);
 if (parts.Length > 0)
 {
  TermStore termStore = GetTermStore(session, parts[0]);
  if (termStore != null && parts.Length > 1)
  {
   twtc.SSPList = termStore.Id.ToString();

   Group group = GetGroup(termStore, parts[1]);
   if (group != null && parts.Length > 2)
   {
    twtc.GroupId = group.Id;

    TermSet termSet = GetTermSet(group, parts[2]);
    if (termSet != null)
    {
     twtc.TermSetList = termSet.Id.ToString();
     configured = true;
    }
   }
  }
 }

 return configured;
}

In order to load MyTermSet, which lives within MyGroup, which resides within MyTermStore, you will use this extension method similar to the following:

protected void Page_Load(object sender, EventArgs e)
{
 bool configured = twtcSpecialty.Configure("MyTermStore/MyGroup/MyTermSet");
 if (!configured)
 {
  throw new ApplicationException("Unable to find target TermSet");
 }
}

The result looks just like the picker from other parts of SharePoint, with functioning look-ahead.


As promised, here is the markup if you want to configure your TaxonomyWebTaggingControl declaratively.


    0d8382cf-2d63-4421-a37a-b9386d0be5c8
    7adeced1-b73e-47dc-b695-82bb28e2f48f

Tuesday, April 27, 2010

SP 2010 - Randomize the order of your search results from FullTextSqlQuery

I had a need recently to perform a query with FullTextSqlQuery but to limit the results and then display them in a random order. The FullTextSqlQuery class does support a RowLimit property, though this would not work in my case because I didn't want to get the exact same answers everytime.

Remembering that LINQ to SharePoint often requires Two-Stage Queries, I decided to employ a similar approach here and pull back the data I could then perform more culling server side.

DataTable searchResults = PerformSearch(query, queryRowLimit);
if (searchResults != null && searchResults.Rows.Count > 0)
{
 int limit = 10; //todo: this should be a webpart property
 var results = (from row in searchResults.AsEnumerable()
       orderby Guid.NewGuid()
       select row).Take(limit);
}

The PerformSearch method is just a standard setup for using FullTextSqlQuery to return a DataTable. Note that I limit my resultset as much as I can with the FullTextSqlQuery.QueryText property to try to avoid hitting a throttling exception.

With the limited results returned, I then want to randomize the order of the records. LINQ allows us to do this the same way we would in SQL, and I just order by a random Guid. Once the results are randomized, we just grab the number of records we need with the Take() extension method.

Friday, April 23, 2010

SP 2010 Managed Metadata TermSet.CreateTerm Throws Error

I've been working on importing a set of Terms into my Managed Metadata Term Store from a 3rd party database. However, I ran into a snag. When I execute the following code, I get an error, "There is already a term with the same default label and parent term."

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
 if (termSet != null && !string.IsNullOrEmpty(termName))
 {
  Term term = null;

  //This throws an exception if the Term doesn't exist
  try
  {
   term = termSet.Terms[termName];
  }
  catch { }

  if (term == null)
  {
   Term t = termSet.CreateTerm(termName, 1033);
   termSet.TermStore.CommitAll();
  }
 }
}

I attached my debugger and found out that even though I was checking for my term, the code would say it wasn't there, even though it was! The problem, was that the specific term causing me problems had an ampersand. The term name I supplied was "Foo & Bar", but the value put into the TermStore actually contained unicode version of the ampersand.

Looking through the documentation, I found this relevant comment:

The name value will be normailized to trim consecutive spaces into one and replace the & character with the wide character version of the character (\uFF06). The leading and trailing spaces will be trimmed. It must be non-empty and cannot exceed 255 characters, and cannot contain any of the following characters ; "<>|&tab.

That was indeed the behavior I was seeing. Thinking I had just found a limitation of the TermSet.Terms collection, I changed my code to this:

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
 if (termSet != null && !string.IsNullOrEmpty(termName))
 {
  Term term = null;

  TermCollection tc = termSet.GetTerms(termName, 1033, true, StringMatchOption.ExactMatch, 1, false);
  if (tc != null && tc.Count > 0)
  {
   term = tc[0];
  }

  if (term == null)
  {
   Term t = termSet.CreateTerm(termName, 1033);
   termSet.TermStore.CommitAll();
  }
 }
}

I ran again, and this time instead of blowing up on just that one case, my code went crazy trying to insert a bunch of different terms that were working fine before and throwing way more exceptions. A little research on this method led me to this post, which suggests the TermSet.GetTerms method does not work in Beta 2, which seems to be what I just discovered as well.

I decided to explore the reference to normalizing term names from the MSDN link. My final pass at the code became:

public static void CreateTermIfNotExists(TermSet termSet, string termName)
{
    if (termSet != null && !string.IsNullOrEmpty(termName))
    {
        Term term = null;

        try
        { 
            string normalizedTermName = TermSet.NormalizeName(termName);
            term = termSet.Terms[normalizedTermName];
        }
        catch { }

        if (term == null)
        {
            Term t = termSet.CreateTerm(termName, 1033);
            termSet.TermStore.CommitAll();
        }
    }
} 

Finally, success! Moral of the story - when you query your TermSet for a specific Term you need to normalize your name first, because that is how it will be stored internally.

Monday, April 19, 2010

SP 2010 - Using LINQ to SharePoint to Find List Items with Specific Managed Metadata Terms

Now that I can effectively use LINQ to access my Managed Metadata Columns, I'd like to only pull back those columns that contain values I need. For single valued Managed Metadata Columns, this is very straightforward:

var examples = from d in context.Examples
      where d.Specialty is TaxonomyFieldValue && ((TaxonomyFieldValue)d.Specialty).Label == "Value One"
               select d;

For multi valued Managed Metadata Columns, my first attempt was a bust. I tried the following expression, but received a compiler error, "An expression tree may not contain an anonymous method expression".

var examples = from d in context.Examples
               where ((TaxonomyFieldValueCollection)d.Specialty).Exists(delegate(TaxonomyFieldValue tfv){
             return tfv.Label == "Value One";
               }) != null
      select d;

The hint here from the compiler is that I can do this as long as I don't use an anonymous method. So I created a method to test for the term I wanted and changed my expression to call this method.

private static bool ContainsMetadataTerm(object o, string termLabel)
{
 bool exists = false;

 if (o is TaxonomyFieldValueCollection)
 {
  TaxonomyFieldValueCollection tfvc = (TaxonomyFieldValueCollection)o;
  exists = tfvc.Exists(delegate(TaxonomyFieldValue tfv)
  {
   return tfv.Label == termLabel;
  });
 }

 return exists;
}

var examples = from d in context.Examples
      where d.Specialty is TaxonomyFieldValueCollection && ContainsMetadataTerm(d.Specialty, "Value One")
      select d;

Now I can query specifically for list items that use specific terms. Keep in mind that this filtering does not happen until after the list items have been pulled down, so you may want to do some additional filtering to narrow the results so you don't run afoul of the new Throttling feature of SharePoint 2010.

Here was the CAML generated by the last LINQ query, which shows that the additional filtering for Term was done after the records were pulled.


  
    
      
        
        0x0100
      
    
  
  
    
    
    
    
    
    
    
    
    2147483647

SP 2010 - Managed Metadata Columns ARE Supported in LINQ to SharePoint

I apparently spoke too soon! You can get to Managed Metadata Columns in LINQ to SharePoint with SPMetal, just not directly off the command line. You need to supply a parameters option.

First you'll want to create your parameters file. I've found two different ways to get the Managed Metadata Column to show up. The first attempt I used this XML in my parameter file.


  
    
      
    
  



I saved this file to metaloptions.xml and then ran the following command.

SPMetal.exe /web:http://mysite /code:SPMySite.cs /namespace:SPMySite /parameters:metaloptions.xml

The output of that command will include a warning, but it seems to have no impact on LINQ working. I think it's purely informational and not relevant to what we are working with.

Warning: All content types for list Form Templates were excluded.

Now when I query with LINQ I see the hidden fields that power my Managed Metadata Column, which look familiar.

On my typed list item object, I now had three new fields:
  • Specialty_0 - String
  • TaxonomyCatchAllColumnCatchAllData - IList
  • TaxonomyCatchAllColumnId - IList

Those fields hold values that look like this, respectively. Unfortunately, this isn't terribly useful.
  • Value One|e203149a-6852-46fb-9d8e-9c21d350068d
  • z4KDDWMtIUSjerk4bQvlyA==|0c7eej633Ee2lYK7KOL0jw==|mhQD4lJo+0adjpwh01AGjQ==
  • 4

I tried another pass on my parameters file. This time I used the following XML.



  
    
      
    
  


On my typed list item object, I now have the one extra Column that I specified in my parameters file. LINQ will create a property for this field that is just an object. However, if you access this property, you can cast it to a TaxonomyFieldValueCollection or TaxonomyFieldValue, as appropriate. This provides exactly the information we wanted:

using (SPMySiteDataContext context = new SPMySiteDataContext("http://mysite"))
{
 var examples = from d in context.examples
      select d;

 foreach (var example in examples)
 {
  if (example.Specialty is TaxonomyFieldValueCollection)
  {
   foreach (TaxonomyFieldValue tfv in (TaxonomyFieldValueCollection)example.Specialty)
   {
    Console.WriteLine(tfv.Label);
   }
  }
  else if (example.Specialty is TaxonomyFieldValue)
  {
   TaxonomyFieldValue tfv = (TaxonomyFieldValue)example.Specialty;
   Console.WriteLine(tfv.Label);
  }
 }
}

SP 2010 - Managed Metadata Columns Not Supported in LINQ to SharePoint?

Today I am playing with LINQ to SharePoint. I created a simple list and added a Managed Metadata Column to my list. I then used SPMetal to generate a DataContext class. My Managed Metadata Column is nowhere to be found. I looked through options for SPMetal to see if maybe it just needed a switch to capture those Managed Metadata Columns, but I don't see one.

So it appears that Managed Metadata Columns have no support in LINQ to SharePoint. They are also not eligible to be Projected Fields. It would seem Microsoft didn't really flesh out all the ways the new Managed Metadata Service might be used.

Update: I figured out how to do this.

Thursday, April 15, 2010

SP 2010 - Creating ListItems with Managed Metadata Columns

Today I needed to create some list items programatically for a list that contained Managed Metadata columns. Since Managed Metadata columns are specialized SPLookups under the covers, it's a little more difficult to set your field value than just assigning the text value of your Term.

Here is a method that will set the value for you, and an example of how to use it. Note that in my example the Managed Metadata column is multi-value, even though the example method is only set up to add a single Term. If you have a single-value Managed Metadata column, you can leave out the TaxonomyFieldValueCollection.

class Program
{
 static void Main(string[] args)
 {
  using (SPSite site = new SPSite("http://mysite"))
  {
   SPList pl = site.RootWeb.Lists["MyList"];
   if (pl != null)
   {
    TaxonomySession session = new TaxonomySession(site);
    TermStore termStore = session.TermStores["Managed Metadata Service"];
    Group group = termStore.Groups["MyGroup"];
    TermSet termSet = group.TermSets["MyTermSet"];

    SPListItem li = pl.AddItem();
    li[SPBuiltInFieldId.Title] = "my new item";

    Term term = termSet.Terms["MyTerm"];
    UpdateListItemTermColumn(li, "MyMetadataField", term);

    li.Update();
   }
  }
 }

 static void UpdateListItemTermColumn(SPListItem li, string fieldName, Term term)
 {
  SPField termField = li.Fields[fieldName];

  TaxonomyFieldValueCollection tfvc = new TaxonomyFieldValueCollection(termField);
  tfvc.Add( new TaxonomyFieldValue(termField));
  li[fieldName] = tfvc;

  TaxonomyField taxonomyField = termField as TaxonomyField;
  if (taxonomyField != null)
  {
   taxonomyField.SetFieldValue(li, term);
  }
 }
}

SP 2010 - Empty Logs Versus Least Permissions Install

Earlier this week I was having problems with my SharePoint logs being empty. At the time, the only fix I could discover was giving my AppPool identity Administrator rights on the machine. Obviously the reason the account lacked such permissions was because I had given it no rights when I created it - the same way I would have when I install MOSS 2007.

Well giving Administrator rights to your AppPool identities in SharePoint 2010 is also a bad thing, as shown by the new Health Monitoring:


However, at this time, I can't seem to find what permission set is needed for ULS to be accessible by my AppPool account. Right now my choices are to upset Health Monitoring or to have logs. Having both is apparently a luxury.

Wednesday, April 14, 2010

SharePoint 2010 - Can't Open Crawled Properties

I have been working on building some custom Search components, which will leverage my own Managed Properties. However, early on in the process I hit a snag.

I created some sample content and then running a full index of my farm. SharePoint was able to discover some new Crawled Properties during this process, which I was hoping to turn into Managed Properies. However, when clicking on any of the new Crawled Properties, I get an error "Unable to cast object of type 'System.DBNull' to type 'System.String'":



Using my newly working SharePoint logs, I found the following error message:

SchemaDatabase.GetSamples:Error occurred when reading [SampleUrl] System.InvalidCastException: Unable to cast object of type 'System.DBNull' to type 'System.String'.     at Microsoft.Office.Server.Search.Administration.SchemaDatabase.GetSamples(CrawledProperty crawledProperty, Int32 sampleCount)

I then opened up Reflector to find what was going on in that method. It turns out to be a very simple method that just calls a stored procedure. I fired up SQL Server Profiler and tracked down this call, which ultimately was breaking the page:

exec dbo.proc_MSS_GetCrawledPropertySamplesByPropertyID @CrawledPropertyId=334,@SampleCount=5

So, as it turns out, this SProc does handle NULLs, just not as robustly as we might want! Here is the SProc, which I found in the Search_Service_Application_PropertyStoreDB_{GUID} database:

CREATE PROCEDURE dbo.proc_MSS_GetCrawledPropertySamplesByPropertyID
@CrawledPropertyId  int,        
@SampleCount        int
AS
        set RowCount @SampleCount
     SELECT
           ( DP.strVal + ISNULL(cast(DP.strVal2 AS nvarchar(2000)), '') ) as 'SampleURL'
     FROM 
            dbo.MSSDocProps as DP         
        INNER JOIN 
            dbo.MSSCrawledPropSamples as CPS
            on CPS.DocId = DP.DocId
     WHERE
         CPS.CrawledPropertyId = @CrawledPropertyId
            AND DP.Pid          = 7
        ORDER BY DP.strVal
        set RowCount 0

Normally I wouldn't dream of altering a SharePoint SProc, but seeing as we are still in beta and I'm approaching a deadline, I decided to make a slight adjustment:

alter PROCEDURE dbo.proc_MSS_GetCrawledPropertySamplesByPropertyID
@CrawledPropertyId  int,        
@SampleCount        int
AS
        set RowCount @SampleCount
     SELECT
           ( ISNULL(DP.strVal,'') + ISNULL(cast(DP.strVal2 AS nvarchar(2000)), '') ) as 'SampleURL'
     FROM 
            dbo.MSSDocProps as DP         
        INNER JOIN 
            dbo.MSSCrawledPropSamples as CPS
            on CPS.DocId = DP.DocId
     WHERE
         CPS.CrawledPropertyId = @CrawledPropertyId
            AND DP.Pid          = 7
        ORDER BY DP.strVal
        set RowCount 0 

Now, when clicking on my Crawled Property, I get a proper page:

Sharepoint 2010 ULS Problems - Logs are Empty!

I've been noticing since my install that SharePoint 2010 seems to not be logging much. It was logging some, but most of the logs were very small. Contrast this with SharePoint 2007 where you could easily generated several hundred kilobytes of logging within a few minute span and you can tell something is wrong.

I tried a lot of things to get to the root of my problem. The question I was trying to answer was why sometimes I'd get logs, and other times not. And why were none of my Correlation IDs found? I tried a lot of different things, including:
  • Disabling the Windows Firewall
  • Stopping and restarting the Windows SharePoint Services Tracing V4 service.
  • Running commands from stsadm or Powershell to see if they could log.
While doing this, I started reading the log entries I was receiving and noticed something. Each of the entries listed the process that was adding the entry. I saw entries from the following processes:
  • STSADM.EXE
  • PowerShell.exe
  • wsstracing.exe
  • vssphost4.exe
  • psconfigui.exe
Surprisingly, I didn't see a single entry for w3wp.exe. I decided to check permissions for the LOGS folder. The user who runs my AppPool, sp_Farm looks like he has almost every permission there is, and should be able to do almost anything to the log files. It didn't make sense.

In frustration I put all my AppPool accounts in the Administrators group and rebooted the machine. Suddenly my logs are going wild.

I'm not entirely sure why my AppPool accounts couldn't write to the logs when they are already in WSS_ADMIN_WPG, but there is clearly some missing permission somewhere. I'll have to break out procmon when I have some free time to figure out what the permission was missing.

Tuesday, April 13, 2010

How Not to Run SharePoint 2010 Successfully

I'm still waiting on some new RAM to arrive in the mail, so currently my development machine for SharePoint 2010 is a Core2Duo 2Ghz laptop running a VM with 2Gigs of RAM. Needless to say, I've gotten used to looking at this:

SP 2010 - Adding Metadata Columns to Content Type Programatically

I have a feature that provisions a few Site Columns and a Content Type that will use those Site Columns. This was easy enough to achieve with some XML packaged into a Feature. However, my Content Type also needed to have a Managed Metadata column, which doesn't work with XML.

The reasons you can't do a Managed Metadata Site Column through XML are the same reasons Lookup columns don't work - the GUIDs that refer to the underlying objects change between environments, so you can't hardcode them into the XML. If you wanted to write the XML by hand to do this and already knew the GUIDs, it would of course work, but that's more effort than it's worth for such a limited solution.

My first thought when trying this was that I knew when I had exported my Content Type that I created through the GUI I found out that my Managed Metadata Site Column as provisioned ended up being several different SPFields under the covers. So in my first pass, I tried to create all of these fields. That ended up breaking my site because I was unable to remove all the SPFields/SPContentTypes after the fact!

Well, it turns out the answer isn't so hard after you try all the wrong ways first. I ended up having my Feature that provisions the base Site Columns and Content Type via XML. I then created a Feature Receiver that creates the Managed Metadata Site Column and then adds it to the Content Type. Here is the code:

public enum Operation
{
 Add,
 Remove
}

[Guid("367cb65f-cd86-440a-8f39-1bfa2a9ab1f6")]
public class MyContentTypeEventReceiver : SPFeatureReceiver
{
 /*
  * On feature activation, we are going to provision a site column that points to the Managed MetaData Service used by this site.
  * We have to do this because managed metadata columns work like lookups do under the covers and are keyed to the store they were
  * created with.
  * 
  * After creating the site column, we will then update our content type to include the new site column.
  */
 public override void FeatureActivated(SPFeatureReceiverProperties properties)
 {
  ProvisionMetadataSiteColumn("Managed Metadata Service", "MyGroup", "MyTermSet", "MyField", "MyFieldGroup", true, true);
  UpdateContentType("MyContentType", "MyField", Operation.Add);
 }

 public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
 {
  UpdateContentType("MyContentType", "MyField", Operation.Remove);
  RemoveSiteColumn("MyField");
 }

 #region Helper Methods

 private bool ProvisionMetadataSiteColumn(string termStoreName, string termGroupName, string termSetName, string fieldName, string fieldGroupName, bool isRequired, bool allowMultipleValues)
 {
  bool added = false;
  using(SPSite site = SPContext.Current.Site)
  {
   if(GetSiteColumn(site.RootWeb, fieldName) == null)
   {
    //this can time out which would cause it to be returned with no term stores; might be a fluke or very situational
    TaxonomySession session = new TaxonomySession(site);

    TermStore termStore = GetTermStore(session, termStoreName);
    if (termStore != null)
    {
     Group group = GetTermGroup(termStore, termGroupName);
     if (group != null)
     {
      TermSet termSet = GetTermSet(group, termSetName);
      if (termSet != null)
      {
       string fieldType = (allowMultipleValues ? "TaxonomyFieldTypeMulti" : "TaxonomyFieldType");

       TaxonomyField field = (TaxonomyField)site.RootWeb.Fields.CreateNewField(fieldType, fieldName);
       field.SspId = termStore.Id;
       field.TermSetId = termSet.Id;
       field.AllowMultipleValues = allowMultipleValues;
       field.Group = fieldGroupName;
       field.Required = isRequired;

       site.RootWeb.Fields.Add(field);
       site.RootWeb.Update();

       added = true;
      }
     }
    }
   }
  }

  return added;
 }

 private void UpdateContentType(string contentTypeName, string fieldName, Operation operation)
 {
  using (SPSite site = SPContext.Current.Site)
  {
   SPContentType contentType = GetContentType(site.RootWeb, contentTypeName);
   if (contentType != null)
   {
    SPField field = GetSiteColumn(site.RootWeb, fieldName);
    if (field != null)
    {
     bool hasFieldLink = HasFieldLink(contentType, field.Id);

     if (operation == Operation.Add && !hasFieldLink)
     {
      SPFieldLink link = new SPFieldLink(field);
      contentType.FieldLinks.Add(link);
      contentType.Update();
     }
     else if (operation == Operation.Remove && hasFieldLink)
     {
      contentType.FieldLinks.Delete(field.Id);
      contentType.Update();
     }
    }
   }
  }
 }

 private bool RemoveSiteColumn(string fieldName)
 {
  bool deleted = false;

  using(SPSite site = SPContext.Current.Site)
  {
   SPField field = GetSiteColumn(site.RootWeb, fieldName);
   if (field != null)
   {
    try
    {
     field.Delete();
     deleted = true;
    }
    catch { }
   }
  }

  return deleted;
 }

 #endregion

 #region Helper Methods to allow NULL checks with SharePoint

 private SPField GetSiteColumn(SPWeb web, Guid fieldId)
 {
  SPField field = null;

  try
  {
   field = web.Fields[fieldId];
  }
  catch { }

  return field;
 }

 private SPField GetSiteColumn(SPWeb web, string fieldName)
 {
  SPField field = null;

  try
  {
   field = web.Fields[fieldName];
  }
  catch { }

  return field;
 }

 private SPContentType GetContentType(SPWeb web, string contentTypeName)
 {
  SPContentType contentType = null;

  try
  {
   contentType = web.ContentTypes[contentTypeName];
  }
  catch { }

  return contentType;
 }

 private bool HasFieldLink(SPContentType contentType, Guid fieldId)
 {
  bool found = false;
  foreach (SPFieldLink fl in contentType.FieldLinks)
  {
   if (fl.Id == fieldId)
   {
    found = true;
    break;
   }
  }

  return found;
 }

 private TermStore GetTermStore(TaxonomySession session, string termStoreName)
 {
  TermStore termStore = null;

  try
  {
   termStore = session.TermStores[termStoreName];
  }
  catch { }

  return termStore;
 }

 private Group GetTermGroup(TermStore termStore, string termGroupName)
 {
  Group group = null;

  try
  {
   group = termStore.Groups[termGroupName];
  }
  catch { }

  return group;
 }

 private TermSet GetTermSet(Group group, string termSetName)
 {
  TermSet termSet = null;

  try
  {
   termSet = group.TermSets[termSetName];
  }
  catch { }

  return termSet;
 }

 #endregion
}

TaxonomySession doesn't show my Term Stores

I ran into an issue yesterday that I haven't completely figured out just yet. Specifically, I had a feature receiver that was querying a Term Store to create a Site Column. Unfortunately, it kept failing. When I attached a debugger to the process it eventually showed me that TaxonomySession.TermStores was empty.

After playing with it a bit, I finally got it to work by opening up Central Administration and going to the Managed Metadata Service page to "warm" up the Taxonomy service. I'm not sure if it was a web service time out issue or not, but that seems likely. I believe that the worker process powering the Taxonomy web service was taking too long to spin up on my resource starved laptop, which was causing TaxonomySession to give up on loading any TermStores.

Unfortunately, ULS shut itself down for some reason during this period, and I was unable to find any logging event that would give me more insight.

If I find out anymore, I'll post about it.

Monday, April 12, 2010

SharePoint 2010 Site Columns for Managed Metadata

I'm still not 100% sure how references to the Managed Metadata Term Stores work. But the Content Type I recently created used one and I'm hoping to get a chance to digest this further soon.

For instance, if I make a feature that pushes out this Site Column, will the GUIDs line up with the external Term Store as long as I use the export/import feature on the Managed Metadata Service?

While I ponder those things, here is the XML for what a site column that refers to the term store. As you can see, it's split across several SPFields.

SharePoint 2010 Content Type Features

Today I decided to create a Content Type for SharePoint 2010. As far as I can tell, while there has been a lot of effort in building in taxonomy improvements, such as tagging, the Content Type is still alive and well and works the same.

The easiest way to create a Content Type feature in MOSS 2007 was to create it in the UI first and then extract it. Normally I use a tool for that, but decided this time I'd give it a shot and write code to do it myself. In the process I could see if the API had changed in this area. It appears to work the same, so experience in MOSS 2007 will apply directly.

Here is the code if you want to try it yourself. The last line will be the contents of your Elements.xml file.

class Program
    {
        static void Main(string[] args)
        {
            GetContentType();
        }

        private static void GetContentType()
        {
            //we'd pull from SPContext.Current.Site, so wouldn't need to do this
            using (SPSite site = new SPSite("http://sharepoint"))
            {
                string ctype = "0x0100420995776E88234093C72FC85E59E4A8"; //from querystring. you could also loop through the RootWeb.ContentTypes collection and find by name
                SPContentTypeId id = new SPContentTypeId(ctype);

                bool includeSiteColumns = true;

                XmlDocument xdElements = new XmlDocument();
                XmlElement xeElementRoot = xdElements.CreateElement("Elements");

                XmlAttribute xaNamespace = xdElements.CreateAttribute("xmlns");
                xaNamespace.Value = "http://schemas.microsoft.com/sharepoint/";
                xeElementRoot.Attributes.Append(xaNamespace);

                xdElements.AppendChild(xeElementRoot);

                SPWeb web = site.RootWeb;
                SPContentType ctMatch = web.ContentTypes[id];
                if (ctMatch != null)
                {
                    XmlDocument xdSchema = new XmlDocument();
                    xdSchema.LoadXml(ctMatch.SchemaXml);

                    //move the field nodes - otherwise we won't pass the validation of an Elements node for our feature
                    //these will become Site Columns
                    XmlNode xnFields = xdSchema.SelectSingleNode("//Fields");
                    if (xnFields != null)
                    {
                        xnFields.ParentNode.RemoveChild(xnFields);

                        if (includeSiteColumns)
                        {
                            XmlDocumentFragment xdfFields = xdElements.CreateDocumentFragment();
                            xdfFields.InnerXml = xnFields.InnerXml;
                            xeElementRoot.AppendChild(xdfFields);
                        }
                    }

                    XmlDocumentFragment xdfContentType = xdElements.CreateDocumentFragment();
                    xdfContentType.InnerXml = xdSchema.FirstChild.OuterXml;

                    //inject FeildRefs to refer to the fields above
                    XmlNode xnFieldRefs = xdElements.CreateElement("FieldRefs");

                    StringBuilder sbFieldRefs = new StringBuilder();
                    foreach (SPFieldLink fr in ctMatch.FieldLinks)
                    {
                        sbFieldRefs.Append(fr.SchemaXml);
                    }

                    XmlDocumentFragment xdfFieldReferences = xdElements.CreateDocumentFragment();
                    xdfFieldReferences.InnerXml = sbFieldRefs.ToString();
                    xnFieldRefs.AppendChild(xdfFieldReferences);

                    xdfContentType.FirstChild.AppendChild(xnFieldRefs);

                    xeElementRoot.AppendChild(xdfContentType);

                    string s = xeElementRoot.OuterXml;
                }
            }
        }
    }

Thursday, April 8, 2010

SharePoint 2010 Random Publishing Error

Just got this lovely error when clicking the Edit button on a page that was pending approval on my publishing site. The joy of beta software!

SharePoint 2010 Publishing Workflows Need the State Service!

In learning SharePoint 2010, I'm trying to take a minimalist approach, and only install services that I need. The problem is I don't yet know what those services are!

So far, on my new installation I have a Managed Metadata Service and a Search Service Application. Thinking to test Search, I edited one of the default pages and then tried to publish it and got the following message:


I suppose that the State Service needs to go on my list of required services. So, I went to install it from the Manage Service Applications page. For some reason, however, it was not listed!


Of course, given my problems with the Managed Metadata Service, I now know that the true magic in SharePoint comes from the Farm Configuration Wizard. I opened it up and was not disappointed when I found this guy:


Which leaves me with the following now running in my environment, and publishing workflows that can display properly.

SharePoint 2010 Pages Used to Create a New Managed Service

Here is the list of pages that are displayed inside the popup windows when you choose to create a new managed service. See this post to see how I got this information.

Application Type Create Application Link URL
Access Database Service/_admin/AccessServerCreateApplication.aspx
Business Data Connectivity/_admin/bdc/managebdcserviceapp.aspx?scenarioid=bdcservice
Excel Calculation Services/_admin/ExcelServerCreateApplication.aspx?scenarioid=ExcelServicesCreateApplication
Managed Metadata Web Service/_admin/ManageMetadataService.aspx
PerformancePoint Service/_admin/CreatePpsMaServiceAppData.aspx?scenarioId=CreatePpsMaApp
SearchQueryAndSiteSettingsService/_admin/search/TopologyAppSettings.aspx
Secure Store Service/_admin/sssvc/createsssvcapplication.aspx?scenarioid=createsssvcapp
User Profile Service/_admin/NewProfileServiceApplicationSettings.aspx?scenarioid=CreateNewProfileServiceApplication
Visio Graphics Service/_admin/VisioServiceApplications.aspx
Web Analytics Web Service/_admin/WebAnalyticsConfigWizardNewAppPage.aspx
Word Automation Services/_admin/WordServerCreate.aspx

SharePoint 2010 Create New Managed Service Popup Windows

While exploring SharePoint I often read through Microsoft's code. With 2010, they added a lot of popup windows, and it's not always obvious which page is actually building the content of those popup windows.

In this particular case, I wanted to know what the page was when I was trying to create a new Service Application. If you click the New button in the Ribbon, you'll get a nice flyout menu with a bunch of services to choose from. But where do they go?

Here is a snippet that will get that answer for you:
class Program
    {
        static void Main(string[] args)
        {
            ShortDisplay();
        }

        internal class HeaderColumn
        {
            public string Title { get; set; }
            public int Width { get; set; }
        }

        static HeaderColumn[] headers = new HeaderColumn[] { 
             new HeaderColumn{ Title="Application Type", Width=35 }, 
             new HeaderColumn{ Title="Create Application Link URL", Width=45 }
        };

        static string headerString = string.Empty;
        static string lineFormatString = string.Empty;

        static List administrationServices = new List();

        private static void FormatHeader()
        {
            for (int i = 0; i < headers.Length; i++)
            {
                headerString += string.Format("{0,-" + headers[i].Width.ToString() + "}", headers[i].Title);
                lineFormatString += "{" + i.ToString() + ",-" + headers[i].Width.ToString() + "}";
            }
        }

        private static void FindAdministrationServices()
        {
            foreach (SPService service in SPFarm.Local.Services)
            {
                IServiceAdministration administration = service as IServiceAdministration;
                if (administration != null)
                {
                    if (!administrationServices.Contains(service))
                    {
                        administrationServices.Add(service);
                    }
                }
            }
        }

        private static List GetAdministrationLinks(IServiceAdministration administration)
        {
            List administrationLinks = new List();

            foreach (Type type in administration.GetApplicationTypes())
            {
                SPPersistedTypeDescription applicationTypeDescription = administration.GetApplicationTypeDescription(type);
                if (applicationTypeDescription != null)
                {
                    SPAdministrationLink createApplicationLink = administration.GetCreateApplicationLink(type);
                    if ((createApplicationLink != null) && !string.IsNullOrEmpty(createApplicationLink.Url))
                    {
                        administrationLinks.Add(createApplicationLink);
                    }
                }
            }

            return administrationLinks;
        }

        public static void ShortDisplay()
        {
            FormatHeader();
            FindAdministrationServices();
            
            Console.WriteLine(headerString);
            foreach (SPService service in administrationServices)
            {
                string displayName = (string.IsNullOrEmpty(service.DisplayName) ? service.TypeName : service.DisplayName);
                //not very reusable, but good enough for now
                if (displayName.Length > 35)
                {
                    int lastDot = displayName.LastIndexOf('.');
                    if (lastDot >= 0 && displayName.Length > lastDot)
                    {
                        displayName = displayName.Substring(lastDot + 1, displayName.Length - lastDot - 1);
                    }
                }

                IServiceAdministration administration = service as IServiceAdministration;
                List administrationLinks = GetAdministrationLinks(administration);
                foreach(SPAdministrationLink createApplicationLink in administrationLinks)
                {
                    Console.WriteLine(lineFormatString, displayName, createApplicationLink.Url);
                }
            }
        }
    }

And here is the output:

SharePoint 2010 AppPool DropDownList

While playing around trying to figure out how SharePoint 2010 populates that wonderful drop down list of AppPools, as seen in the screeshot just below, I noticed that all the classes it uses are marked internal sealed.


Since I wanted to learn more about how this control worked, I decided to use reflector to poke around. Here is the small amount of info I discovered of the items in the list:


And here is the code that produces that output.

public class Program
    {
        static void Main(string[] args)
        {
            DiscoverAppPools();
        }

        public class AppPoolInfo
        {
            public string Name { get; set; }
            public string IISObjectName { get; set; }
            public IdentityType IdentityType { get; set; }
            public string Username { get; set; }
        }

        //Queries the farm to find the list of AppPools to display in an IisWebServiceApplicationPoolSection control
        public static void DiscoverAppPools()
        {
            List discoveredAppPools = new List();

            Assembly assembly = Assembly.Load("Microsoft.SharePoint, Version=14.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
            Type typeSPIisWebServiceSettings = assembly.GetType("Microsoft.SharePoint.Administration.SPIisWebServiceSettings");

            /*
             * There are a few other interesting properties that we aren't pulling, but might be handy to peek at.
             * oSettings.IisSiteName
             * oSettings.HttpPort
             * oSettings.HttpsPort
             * oAppPool.Id
             * oAppPool.Status
             * oAppPool.ProcessAccount.SecurityIdentifier.Value
             */
            object oSettings = SPFarm.Local.GetObject("SharePoint Web Services", SPFarm.Local.Id, typeSPIisWebServiceSettings);
            if (oSettings != null)
            {
                Type typeSPIisWebServiceApplicationPoolCollection = assembly.GetType("Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPoolCollection");
                ConstructorInfo constructor = typeSPIisWebServiceApplicationPoolCollection.GetConstructor(
                    BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeSPIisWebServiceSettings }, null);

                object oAppPools = null;                
                if (constructor != null)
                {
                    oAppPools = constructor.Invoke(new object[] {oSettings});
                    if (oAppPools != null)
                    {
                        Type typeSPIisWebServiceApplicationPool = assembly.GetType("Microsoft.SharePoint.Administration.SPIisWebServiceApplicationPool");

                        IEnumerable appPools = (IEnumerable)oAppPools;
                        if (appPools != null)
                        {
                            IEnumerator enumerator = appPools.GetEnumerator();
                            while (enumerator.MoveNext())
                            {
                                AppPoolInfo api = new AppPoolInfo();

                                object oAppPool = enumerator.Current;
                                PropertyInfo piName = typeSPIisWebServiceApplicationPool.GetProperty("Name");
                                if (piName != null)
                                {
                                    api.Name = (string)piName.GetValue(oAppPool, null);
                                }

                                PropertyInfo piIisObjectName = typeSPIisWebServiceApplicationPool.GetProperty("IisObjectName", BindingFlags.Instance | BindingFlags.NonPublic);
                                if (piIisObjectName != null)
                                {
                                    api.IISObjectName = (string)piIisObjectName.GetValue(oAppPool, null);
                                }

                                //not terribly useful; really only gives a SID. Maybe good if you wanted their credentials as a SecureString
                                //PropertyInfo piProcessAccount = typeSPIisWebServiceApplicationPool.GetProperty("ProcessAccount");
                                //if (piProcessAccount != null)
                                //{
                                //    object o = piProcessAccount.GetValue(oAppPool, null);
                                //}

                                PropertyInfo piCurrentIdentityType = typeSPIisWebServiceApplicationPool.GetProperty("CurrentIdentityType", BindingFlags.Instance | BindingFlags.NonPublic);
                                if (piCurrentIdentityType != null)
                                {
                                    api.IdentityType = (IdentityType)piCurrentIdentityType.GetValue(oAppPool, null);
                                }

                                PropertyInfo piManagedAccount = typeSPIisWebServiceApplicationPool.GetProperty("ManagedAccount", BindingFlags.Instance | BindingFlags.NonPublic);
                                if (piManagedAccount != null)
                                {
                                    object oManagedAccount = piManagedAccount.GetValue(oAppPool, null);
                                    if (oManagedAccount != null)
                                    {
                                        PropertyInfo piUsername = oManagedAccount.GetType().GetProperty("Username");
                                        if (piUsername != null)
                                        {
                                            api.Username = (string)piUsername.GetValue(oManagedAccount, null);
                                        }
                                    }
                                }

                                discoveredAppPools.Add(api);
                            }
                        }
                    }
                }
            }

            Console.WriteLine("SharePoint knows about the following AppPools");
            foreach(AppPoolInfo api in discoveredAppPools)
            {
                Console.WriteLine("\nAppPool: {0}", api.Name);
                Console.WriteLine("  IIS Object Name: {0}", api.IISObjectName);
                Console.WriteLine("  Identity Type: {0}", api.IdentityType);
                Console.WriteLine("  Username: {0}", api.Username );
            }
        }
    }

Random SharePoint 2010 Coding Discoveries - AppPool DropDownList

While tinkering with my Managed Metadata Service issues this week, I noticed that the drop down list of Application Pools in SharePoint does not always match the Application Pools listed in IIS. I decided to dig and find out how SharePoint populates that list.
I started by opening the page that creates the Managed Metadata Service, ManageMetadataService.aspx, located at C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\TEMPLATE\ADMIN, since I knew it had the list of AppPools on it already. I found the control in there, IisWebServiceApplicationPoolSection, which lives in IisWebServiceApplicationPoolSection.ascx.

That class for that control exists in the C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\14\ISAPI\Microsoft.SharePoint.dll assembly. Inside it I found the method that was doing the work, PopulateApplicationPoolDropDownList, and gave it a look. Here are the important lines:

SPIisWebServiceSettings settings = SPIisWebServiceSettings.Default;
if (null != settings) {
 foreach (SPIisWebServiceApplicationPool pool in settings.ApplicationPools)  {
 }
}

The key there is that the property, SPIisWebServiceSettings.Default, will ultimately return this:

return (SPIisWebServiceSettings) farm.GetObject("SharePoint Web Services", farm.Id, typeof(SPIisWebServiceSettings));

That method, as you might guess, simply returns an SPPersistedObject, which is just a serialized object stored as an XML blob in the configuration database. In other words, the list is not being pulled directly from IIS, hence why SharePoint thinks I have an AppPool that IIS does not!

A little more investigating has led me to this little gem, which I will have to look into further in the future, as it may help me fill in some of the missing pieces of how the AppPools eventually make their way into IIS:

SPIisWebServiceApplicationPoolProvisioningJobDefinition.Execute(), which calls SPIisWebServiceApplicationPool.ProvisionLocal()

I am done exploring for now, but at least I know I'm not crazy - just because SharePoint thinks there is an AppPool doesn't mean there actually is one! My current theory is that the job to create my AppPool didn't run or errored out.

Managed Metadata Service fails to work and missing AppPool, Round 2

As you might recall from this post, I have been trying to figure out why provisioning a Managed Metadata Service keeps failing. I know that my AppPool isn't getting created, which is strange, as SharePoint is usually good about creating AppPools whenever it asks you if you'd like one.


As my first two attempts both attempts ended with the same issue, I decided to try some different approaches. For my third attempt, I still chose to create the service myself (as opposed to using the Farm Configuration Wizard), but rather than selecting a new AppPool, I chose an existing one, SharePoint Web Services. Surprise, surprise, that doesn't work either. Exact same error message, even though we know this AppPool exists.

In frustration, I restored from a snapshot again, but this time used the Farm Configuration Wizard with only the Managed Metadata Service checked. At the step for creating a Site Collection, I clicked Skip. And magically, it worked. How interesting that I can't use the GUI to provision a service but the wizard can. I decided to poke around and see what was different.

So far I've noticed:

In IIS
  • An AppPool with a GUID name, running with the Managed Account I specified in the wizard.
  • If I right click on the AppPool and choose "View Applications" I see a Virtual Directory that points to  C:\Program Files\Microsoft Office Servers\14.0\WebServices\Metadata.
  • The Virtual Directory lives under the SharePoint Web Services site.
  • An AppPool named "SharePoint - 80", also running with that Managed Account.
  • If I right click on this AppPool and choose "View Applications", I see a site that points to C:\inetpub\wwwroot\wss\VirtualDirectories\80
In Central Administration
  • A new Web Application, "SharePoint - 80", corresponding to the site in IIS
  • There are no site collections in this Web Application, which is to be expected since I skipped that step in the wizard.
  • Under Manage Service Applications, my Managed Metadata Service is now listed.
  • Clicking on the Managed Metadata Service under Manage Service Applications displays the management screen without any errors.
In SQL Server
  • A new database, named "Managed Metadata Service_{GUID}", with a very unattractive GUID in the name, of course.

So, there are two possible choices for what caused the magic, and it could be both of them. It's possible I have to create the AppPool myself. I find this unlikely, however, as the screen to provision the Managed Metadata service does prompt me to create the AppPool. I'm going to take SharePoint at its word that it is capable of creating this. The other possibility is that even if there is no existing Site Collection in your Farm, you at least need a Web Application created before you can provision the Managed Metadata service.

I plan on digging through the code of the Farm Configuration Wizard later to determine which of those steps was key.

Wednesday, April 7, 2010

SharePoint 2010 Installation has Missing server side dependencies errors

I've configured SharePoint with a PowerShell script at least 4 times in the last 24 hours from a clean install, and only once has it NOT had the Missing Server side dependency errors. I don't even have any services installed in the Farm yet, so the common fix of "tricking" SharePoint by opening one of the Search configuration pages is not doable. Furthermore, the list of web parts that do not have their tp_Assembly populated is quite long. The annoying part is that SharePoint actually does know where the classes live; it just can't seem to populate that column in other tables:

select nvarchar7 'Web Part', nvarchar8 'Assembly', nvarchar9 'Class' from alluserdata where tp_listid in (select tp_id from alllists where tp_title = 'Web Part Gallery')
I don't know if the configuration of SharePoint is just that flakey, or if it's specifically tied to trying to automate the configuration rather than using the SharePoint 2010 Products Configuration Wizard, but it's very frustrating. Right now the only consistent configuration experience I can get is from creating the databases with Powershell and then running the SharePoint 2010 Products Configuration Wizard to do the rest.

I've read that this is a bug that may be fixed in the RTM version of SharePoint 2010, but time will tell.

Tuesday, April 6, 2010

Managed Metadata Service fails to work and missing AppPool

Took my first pass at installing the Managed Metadata Service. It of course failed, with a very common error of "The Managed Metadata Service or Connection is currently not available. The Application Pool or Managed Metadata Web Service may not have been started. Please Contact your Administrator."



First thing I checked was that the service was running by going to Services on Server. It was not, so I started it, which gave no errors. Checked again and still got the error. I decided the Application Pool could be the problem since I selected to create a new one to use during installation of the Managed Metadata Service. Unfortunely, the Application Pool is not in IIS 7. I have no idea why, as if I try to provision another service it shows my Application Pool in the drop down list of available ones.

At this point the only thing I'm sure of is that I missed installing the WCF patch, KB976462. Going to reload from a snapshot and try again.

Document Sets

Interesting post from Stan Liu on Document Sets. Specifically, I find this idea very interesting, since it is very similar to a concept used in 2007 to have auto-inherited values for columns based on their parent folder.
If you have a few columns that are Managed Metadata type (this is a new column type that looks up taxonomy terms from the new Term Store), you can choose those to be Shared Columns so that all the documents within that Document Set will inherit those same attributes. 

Installation Account can't run SharePoint PowerShell cmdlets, Round 2

Part 1

Okay, explored this a little further since the error about needing to be Farm Administrator to run some cmdlets when you were, in fact, a Farm Administrator was bugging me.

I created a new account, sp_Farmadmin2. I added him to the Administrators group on my SharePoint server. Logged in as him and tried to run the SharePoint 2010 Management Shell and got my familiar error, "The local farm is not accessible. Cmdlets with FeatureDependencyId are not registered". Note that it would also not work if I right clicked and chose "Run as Administrator".

I then ran the Add-SPShellAdmin command and specified sp_Farmadmin2. Switched back to sp_Farmadmin2 and tried to run the SharePoint 2010 Management Shell again. Same error. However, this time when I right clicked and chose "Run as Administrator", it worked.

Note that sp_Farmadmin2 is NOT a member of the WSS_Admin_WPG group in Active Directory. Nor are they a member of the the Farm Administrators group in SharePoint, though as an administrator for the machine, they can get into Central Administration.


It seems there are only three keys to getting the cmdlets to work:
  1. Must be an administrator on the SharePoint server.
  2. Must be in the SharePoint_Shell_Access role in the SP_Config database.
  3. Must right-click and chose "Run as Administrator" to start the SharePoint 2010 Management Shell.

SharePoint 2010 FBA

Apparently FBA in 2010 requires setting up your Web Application to use Claims Based Authentication rather than Classic Mode Authentication.

I can't help but have flashbacks to the presentation on Claims Based Authentication in the SharePoint Ignite sessions. Even though everyone in that room was an experienced SharePoint implementer/developer, there was a sea of mostly bewildered faces. I'm hoping that was just due to how detailed the presentation was and that the configuration of FBA is going to be much easier than trying to understand all the nuances of the theory of Claims Based Authentication.

Relevant linkage: Chun Liu on SharePoint

Monday, April 5, 2010

Automating SharePoint 2010 Configuration

Found a great blog post from Gary Lapointe on automating some of the SharePoint 2010 configuration. Gave his script a whirl.

First pass I mistyped the farm account password and I got a halfway installed Farm. I ended up blowing it away to start again clean.

Second pass it installed everything with no error messages. Then I logged into the newly created Central Administration site and found the following:



I clicked the link and was taken to the SharePoint Health Analyzer Reports screen, which shows a problem with the SPTimerService.



Clicking to see the details showed this:



So that's pretty interesting, since the script from Gary had this line in it:
Install-SPFeature -AllExistingFeatures

I'll have to give it another run and also compare the script to what is in AutoSPInstaller, the so-called "Franken-script" over on CodePlex.

SharePoint 2010 Installation Account can't run SharePoint PowerShell cmdlets

With SharePoint 2007 it was common practice to use an installation account rather than your personal one to install SharePoint. This was due to some quirks with the way installation worked where users added after the fact, even as Farm Administrators, would get permission errors in places like SSP Admin.

Well, trying that with SharePoint 2010 has had some interesting side effects. I created a user, sp_Farmadmin, dropped him in the Administrators group for my SharePoint box and then gave him sysadmin in SQL Server. In theory this should be securityadmin and dbcreator, according to Technet, but surely I didn't give them too little permission to do the job.

I ran the install as sp_Farmadmin, then ran the SharePoint 2010 Products Configuration Wizard. After it finished I opened Central Administration and all looked good.

When I tried to open the SharePoint 2010 Management Shell, however, I got the error "The local farm is not accessible. Cmdlets with FeatureDependencyId are not registered".




I googled a bit and found some hints that it had to do with permissions. I found this gem on Technet:
A user cannot run Windows PowerShell cmdlets in a farm unless you have used this cmdlet to add the user to the SharePoint_Shell_Access role. When a user has been added to the SharePoint_Shell_Access role and is a member of the WSS_Admin_WPG local group, then the user can run Windows PowerShell in a multiple-server farm environment.

I went and checked. Yep, sp_Farmadmin was a member of WSS_Admin_WPG. And in SQL Server, the login had a User Mapping to dbo in the SP_Config database. Within that database, dbo owned the SharePoint_Shell_Access role, but wasn't a member.

Seemed like a slam dunk from there. I logged into the machine again as the domain administrator account (who, as an admin with no specific privileges can run the SharePoint 2010 Management Shell successfully). I ran the Add-SPShellAdmin command and got a nice error "Cannot add domain\sp_Farmadmin to SharePoint_Shell_Access role of the database SP_Config. Possible cause of the error is when the user is the owner of the role."




So they can't be a member of a role they own. I confirmed it by trying to manually add the user to the role in SQL Server. Definitely can't be done. That's inconvenient. I tried to remove the User Mapping, but you can't do that, either. After playing with it a bit I realized the problem is that the user who created the database is mapped to dbo, and since dbo is a privileged identity, you do not want just anybody to be mapped to dbo.

As it turns out, the key to fixing this is to just make sure that the SP_Config database is created by the administrator and not by any special accounts you might want to use for administering SharePoint. Since it's preferable to control the naming of the Central Administration content database anyway, you can kill two birds with one stone by using PowerShell to create the config database using the New-SPConfigurationDatabase command. You can then run the SharePoint 2010 Products Configuration Wizard and when prompted to modify server farm settings, make sure you do not disconnect from the server farm.

Once SharePoint is configured, make sure to use Add-SPShellAdmin to let your users run the SharePoint Powershell cmdlets. This will add them to the SharePoint_Shell_Access role in the SP_Config database. It's supposed to also add them to the WSS_Admin_WPG role as well, but that didn't happen for me so I had to do it manually. Once both of these permissions were in place, the user should then be able to run SharePoint cmdlets. Keep in mind some of the cmdlets require Farm Administrator privileges, although just adding them to the Farm Administrator group is apparently not enough...



I'll keep digging on that one.

Friday, April 2, 2010

SharePoint 2010 Products Configuration Wizard errors

Was trying to install SharePoint 2010 with an installation account, sp_Farmadmin. Got an error message saying that "An error has occurred while validation the configuration settings. The errorData argument cannot be null or zero length."

I had to dig through the SharePoint logs in the 14-hive to figure this out, but it ended up being that the installation account didn't have permissions in SQL Server to provision the SP_Config database. Gave my user sysadmin permission in the database (should have beeen securityadmin and dbcreator, actually), and tried again and succeeded.