Welcome to MSDN Blogs Sign in | Join | Help

HowTo: Create a quizzing application with SharePoint 2007

With SharePoint 2007 "Survey" list template, I recently created a quizzing application.  The thing with survey list is that it stack ranks the questions taking into account how many of the respondents favored each options within that question.  But for a quizzing application, we need to be able to stack rank the respondents on how many of their options where correct.  "Survey" list template, traditionally (by-design) cannot be used for a quiz requirement, but will some careful planning and custom-coding, I was able to make use of the SharePoint survey list template to organize a quiz within our organization.

Some preliminary work:

First, I had to setup a list that will host my questions, choices and the correct answer.  I also setup a custom field to figure-out if a question is already used in a quiz and to compute results for a particular quiz.  Below is a screen-shot of how this list looks like:

image

The tool to create quiz and compute results:

I created this tool based on the "business requirement" we had.  There are several hard-coding etc.,  However, it could be extended to suit your needs.  All that's really needed is to understand how the choices selected by respondents are to be read and computed to formulate their individual scores.

Tool UI to create the quiz:

image

The section highlighted above, is going to create a list of type survey.  It will use the "Quiz Questions and Answers" list as the source for filling itself up with questions and choices.  In my case, the quiz needs to be within a sub site.  I type in the full URL of the sub site and click "Get Lists" to populate the drop-down control.  Then provide the quiz title and description (actually the survey title and description) and specify the number of questions I need to consume in this quiz from the source list.  The "Day (1, 2, 3 etc.,)" is a parameter that I use to mark the questions in the source list "Quiz Questions and Answers" to denote they are already used.  This is mapped to the column "Used" shown in the screen-shot of "Quiz Questions and Answers" list above.  Once the quiz list is created after clicking the "Create Quiz" button shown above, the source list will look like the below screen-shot:

image

As we can see, the first 5 questions' "Used" column is now filled with 1, which is my custom parameter to denote those questions are used in day 1 quiz.  I also use this to compute results, specifically to see which questions I need to pick up from the entire list - this way it's going to be optimized and the computation would be much faster with minimum overhead on the server.

Code:

The GetLists() method returns all the lists that are not marked "hidden".  This is the method that will return our q & a list and populate the drop-down control.

void GetLists()
        {
            try
            {
                comboBox1.Items.Clear();
                richTextBox1.Clear();
                using (SPSite site = new SPSite(textBox1.Text.Trim()))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        SPListCollection lists = web.Lists;
                        foreach (SPList list in lists)
                        {
                            if (list.Hidden != true)
                                comboBox1.Items.Add(list.Title);
                        }
                        comboBox1.SelectedIndex = 0;
                    }
                }
            }
            catch (Exception _e)
            {
                richTextBox1.Text += "Message: " + _e.Message + Environment.NewLine + Environment.NewLine +
                    "Stack Trace: " + _e.StackTrace;
            }
        }

The CreateQuiz() method creates the quiz list (of type Survey list).  The most important part here is the "foreach" loop that populates the questions and answers in the form of a survey question and choices.

void CreateQuiz()
        {
            richTextBox1.Clear();
            try
            {
                using (SPSite site = new SPSite(textBox1.Text.Trim()))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        int counter = 0;
                        int fldCnt = 3;
                        string question = string.Empty;
                        StringCollection choices = new StringCollection();
                        Guid quizId = web.Lists.Add(textBox2.Text.Trim(), textBox3.Text.Trim(), SPListTemplateType.Survey);
                        richTextBox1.Text += "Quiz list created.  Id: " + quizId.ToString() + Environment.NewLine;
                        SPList quiz = web.Lists[quizId];
                        quiz.ReadSecurity = 2;
                        quiz.WriteSecurity = 2;
                        quiz.AllowMultiResponses=false;
                        richTextBox1.Text += "ReadSecurity set to: " + quiz.ReadSecurity.ToString() + Environment.NewLine;
                        richTextBox1.Text += "WriteSecurity set to: " + quiz.WriteSecurity.ToString() + Environment.NewLine;
                        quiz.BreakRoleInheritance(false);
                        quiz.Update();
                        richTextBox1.Text += "Reading \"Unused\" items from source list" + Environment.NewLine;                        
                        foreach (SPListItem sListItem in web.Lists[comboBox1.SelectedItem.ToString()].Items)
                        {
                            if (counter < Convert.ToInt32(textBox4.Text.Trim()))
                            {
                                if (sListItem["Used"].ToString().Equals("0"))
                                {
                                    question = sListItem["Question"].ToString();
                                    choices.Add(sListItem["Option1"].ToString());
                                    choices.Add(sListItem["Option2"].ToString());
                                    choices.Add(sListItem["Option3"].ToString());
                                    choices.Add(sListItem["Option4"].ToString());
                                    choices.Add(sListItem["Option5"].ToString());
                                    counter++;
                                    string fldName = quiz.Fields.Add(question, SPFieldType.Choice, true, false, choices);
                                    SPFieldChoice choiceFld = (SPFieldChoice)quiz.Fields[fldCnt];
                                    choiceFld.EditFormat = SPChoiceFormatType.RadioButtons;
                                    choiceFld.Update();
                                    fldCnt++;
                                    richTextBox1.Text += "Added quiz question and options..." + Environment.NewLine;
                                    quiz.Update();
                                    choices.Clear();
                                    sListItem["Used"] = textBox6.Text.ToString();
                                    sListItem.Update();
                                    web.Update();
                                }
                            }
                            else
                            {
                                break;
                            }                            
                        }
                        if (textBox1.Text.EndsWith("/"))
                        {
                            richTextBox1.Text += "Quiz URL: " + HttpUtility.UrlPathEncode(textBox1.Text.Trim() + "Lists/" + textBox2.Text.Trim() + "/NewForm.aspx") + Environment.NewLine;
                        }
                        else
                        {
                            richTextBox1.Text += "Quiz URL: " + HttpUtility.UrlPathEncode(textBox1.Text.Trim() + "/Lists/" + textBox2.Text.Trim() + "/NewForm.aspx") + Environment.NewLine;
                        }
                    }
                }
                richTextBox1.Text += "All suskess...!";
            }
            catch(Exception _e)
            {
                richTextBox1.Text += "Message: " + _e.Message + Environment.NewLine + Environment.NewLine +
                    "Stack Trace: " + _e.StackTrace;
            }
        }

The permissions:

After the quiz list is created there's one other thing I had to do, which is to setup permissions.  In my case, it was a quiz organized within my org so we had a security distribution list covering all the prospective quiz takers.  So, I created a new "Permission Level" in SharePoint with the following options turned on:

  • Add Items - Add items to lists, add documents to document libraries, and add Web discussion comments.
  • View Items - View items in lists, documents in document libraries, and view Web discussion comments.
  • View Application Pages - View forms, views, and application pages. Enumerate lists.
  • View Pages - View pages in a Web site.
  • Open - Allows users to open a Web site, list, or folder in order to access items inside that container.

Now I have my custom permission level with the kind of permission I need.  I also have a new quiz list created.  I simply assign the security distribution list to the quiz list with the custom permission level I created.  I can do this through code too, however, do to time constraint (I built this quizzing application in just under 4 hrs time), I opted to do this manually.

Well, in the code, the Read/Write Securities for the survey list are set to 2, which means "Users have Read access only to items that they create" and "Write only my items" respectively.  However, once a user answers all questions and submits the quiz, he/she can always edit it at anytime or even delete his/her response and attend a quiz again.  One of my "business requirement" was to prevent this scenario and custom permission levels just did that trick.  If the above is done then when a user responds to the quiz and tries to view their response - they can, however they will not be able to edit/delete their response.

"And the winner is..." - the computation part:

Now, coming to the quiz winner computation part as shown below:

image

On clicking "Get Quiz Lists" the drop-down will be populated with lists of type survey.  And clicking "Compute Results" will create another list sorted according to the total score and the earliest response.  This results list is also available as an STP file enclosed in this post.

Code:

The GetQuizLists() method populates the quiz lists drop-down with survey lists.

void GetQuizLists()
        {
            try
            {
                comboBox2.Items.Clear();
                using (SPSite site = new SPSite(textBox5.Text.Trim()))
                {
                    using (SPWeb web = site.OpenWeb())
                    {
                        foreach (SPList list in web.Lists)
                        {
                            if (list.BaseType == SPBaseType.Survey)
                            {
                                comboBox2.Items.Add(list.Title);
                            }
                        }
                        comboBox2.SelectedIndex = 0;
                    }
                }
            }
            catch (Exception _e)
            {
                richTextBox1.Text += "Message: " + _e.Message + Environment.NewLine + Environment.NewLine +
                    "Stack Trace: " + _e.StackTrace;
            }
        }

The ComputeResults() method is the most important method that computes who the winner is depending on the Nth day of the quiz.  It compares individual quiz takers' response with the correct answer and increments a counter to compute the total score for an individual.  It does this for all the quiz takers and finally creates another results list using a template (enclosed).  This section in the code is where you'll find multiple hard-coding stuffs, so just go through the code and customize it per your need.

void ComputeResults()
        {
            richTextBox1.Clear();
 
            using (SPSite site = new SPSite(textBox5.Text.Trim()))
            {
                using (SPWeb web = site.OpenWeb())
                {
                    int score = 0;
                    SPList sList = web.Lists["Quiz Questions and Answers"];
                    SPQuery query = new SPQuery();
                    query.Query = "<Where><Eq><FieldRef Name='Used'/><Value Type='Number'>" +
                        Convert.ToInt32(textBox7.Text.Trim()) + "</Value></Eq></Where>";
                    SPListItemCollection sItems = sList.GetItems(query);
                    string name = string.Empty;
                    DateTime datetime = DateTime.Now;
                    string quizTitle = comboBox2.SelectedItem.ToString();
                    SPList resultList = null;
                    foreach (SPListItem response in web.Lists[comboBox2.SelectedItem.ToString()].Items)
                    {
                        foreach (SPListItem sItem in sItems)
                        {
                            name = (string)response["Created By"];
                            name = name.Substring(name.IndexOf("#") + 1);
                            datetime = (DateTime)response["Modified"];
                            string ans = (string)response[sItem["Question"].ToString()];
                            if (ans.Equals(sItem["Correct Answer"].ToString().Trim()))
                            {
                                score++;
                            }
                        }
 
                        SPListTemplate listTemplate = site.GetCustomListTemplates(web)["quiz_results"];
                        Guid newResultList = new Guid();
                        try
                        {
                            newResultList = web.Lists["Results for " + quizTitle].ID;
                        }
                        catch (ArgumentException _ae)
                        {
                            newResultList = web.Lists.Add("Results for " + quizTitle, "quiz results", listTemplate);
                            web.Update();
                        }
                        resultList = web.Lists[newResultList];
                        SPListItem resultItem = resultList.Items.Add();
                        resultItem["Login"] = name;
                        resultItem["Points"] = score;
                        resultItem["DateTime"] = datetime;
                        resultItem.Update();
                        score = 0;
                    }
                    richTextBox1.Text += "All done... check \"http://mossquiz" + resultList.DefaultViewUrl + "\" for results" + Environment.NewLine;                    
                }
            }
           
 
            try
            { }
            catch (Exception _e)
            {
                richTextBox1.Text += "Message: " + _e.Message + Environment.NewLine + Environment.NewLine +
                    "Stack Trace: " + _e.StackTrace;
            }
        }

End Note:

SharePoint being a collaborative and sharing platform should have provisioned such a quizzing template out of the box, but sadly there isn't one.  However, there's lot of demand for such utilities because most organizations conduct quiz/test of these sort a lot targeted towards readiness and learning.  The above solution can also be built into a feature and deployed as a solution package, however, that will be a larger implementation as we might have to automate few things and provide a web interface to manage it.

I hope this post and the tool was useful to you as it was highly useful to me (have used it in more than one quizzing events and so far am successful :) ).

Keep exploring!

How do I use SecureString type in SharePoint 2007?

SecureString is the type that's used for passwords in SharePoint 2007.  You might find its use when you are automating some "higher level" administration operation (like creating a web application for example).  How do you use it is shown below:

   1: private static SecureString ConvertString(string strPwd)
   2: {
   3:     if (strPwd == null)
   4:         return null;
   5:     SecureString strSecurePassword = new SecureString();
   6:     foreach (char ch in strPwd)
   7:         strSecurePassword.AppendChar(ch);
   8:     strSecurePassword.MakeReadOnly();
   9:     return strSecurePassword;
  10: }

The above method will return the type SecureString that we can pass to the calling application like below:

   1: SPWebApplicationBuilder oBuilder = new SPWebApplicationBuilder(SPFarm.Local);
   2: oBuilder.ApplicationPoolPassword = ConvertString("test");

SecureString finds its place when you deal with the following APIs:

SPFarm.Create

SPFarm.Open

SPSecurity.SetApplicationCredentialKey

SPEncryptedString.UpdateSecureStringValue

SPPeoplePickerSearchActiveDirectoryDomain.SetPassword

SPEncryptedString.SecureStringValue

SPProcessIdentity.SecurePassword

SPWebApplicationBuilder.ApplicationPoolPassword

SPRestoreSettings.FarmAdminPassword

SPMetabaseManager.ProvisionIisApplicationPool

PasswordTextBox.SecurePassword - this is a control.

These are the APIs that I could see uses SecureString, there might be others as well.  Just came across an automation scenario lately where I had to create multiple web applications and wondered how to I set the password of type SecureString.

Invested around 20-30 minutes of time figuring it out, so thought if I post this, it might as well be helpful to others :)

Keep hacking!

Using Post Caching Substitution in SharePoint 2007 Web Parts

There are good number of articles that explains the different caching option Microsoft Office SharePoint Server 2007 provides and ways to leverage them to achieve better site performance.  However, there are scenarios where you might want to implement output caching on your site/page, but have some controls (Web Parts, User Control) excluded from caching so that they dynamically have their content updated on every request.

These are the kind of scenarios where you'd be using Post Caching Substitution.  I first setup, output caching on a SharePoint 2007 site that was built using collaboration site definition.  I created my own caching profile and below are the settings I chose when I created it:

image

I then enabled output cache and set this up for both anonymous and authenticated cache profiles as shown below:

image

To verify if my caching works, I wrote a small web part that shows the current date/time whenever it runs.  Code below:

   1: using System;
   2: using System.Runtime.InteropServices;
   3: using System.Web.UI;
   4: using System.Web.UI.WebControls;
   5: using System.Web.UI.WebControls.WebParts;
   6: using System.Xml.Serialization;
   7:  
   8: using Microsoft.SharePoint;
   9: using Microsoft.SharePoint.WebControls;
  10: using Microsoft.SharePoint.WebPartPages;
  11:  
  12: namespace HellWorld
  13: {
  14:     [Guid("<random guid>")]
  15:     public class HellWorld : System.Web.UI.WebControls.WebParts.WebPart
  16:     {
  17:         public HellWorld()
  18:         {
  19:         }
  20:  
  21:         protected override void Render(HtmlTextWriter writer)
  22:         {
  23:             writer.WriteLine(DateTime.Now.ToString());            
  24:         }
  25:  
  26:         protected override void CreateChildControls()
  27:         {
  28:             base.CreateChildControls();
  29:         }
  30:     }
  31: }

After deploying this web part to my site collection, if I refresh the page or open the site using a new browser session, I'll get to see the date/time that I saw the first time I visited the page.  This happens till the cache expiration time expires (in my case it was 3600 seconds).  Now, that caching happens, I had to implement post cache substitution to not cache another web part that also returns the current date/time.

The actual code the renders the current date/time is the same, but it is rendered in a different mechanism.  First, I added another class file to my current web part project and implemented the response substitution call back method.  Sample code below:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Text;
   4: using System.Globalization;
   5: using System.IO;
   6: using System.Reflection;
   7: using System.Web;
   8: using System.Web.UI;
   9: using System.Web.UI.WebControls;
  10:  
  11: namespace PCSWebPart
  12: {
  13:     public abstract class DontCachePlease
  14:     {
  15:         private ConstructorInfo _writerConstructor;
  16:         private HttpContext _context;
  17:  
  18:         protected DontCachePlease()
  19:         {}
  20:  
  21:         protected HttpContext Context
  22:         {get { return _context; }}
  23:  
  24:         public void Render(HttpContext context, HtmlTextWriter writer)
  25:         {
  26:             if (context == null)
  27:                 throw new ArgumentNullException("context");
  28:             if (writer == null)
  29:                 throw new ArgumentNullException("writer");
  30:             Type writerType = writer.GetType();
  31:             Type[] constructorArgs = new Type[] { typeof(TextWriter) };
  32:             _writerConstructor = writer.GetType().GetConstructor(constructorArgs);
  33:             if (_writerConstructor == null)
  34:                 throw new InvalidOperationException("The HtmlTextWriter does not have a public constructor taking in a TextWriter");
  35:             HttpResponseSubstitutionCallback subCallback = new HttpResponseSubstitutionCallback(this.RenderCallback);
  36:             context.Response.WriteSubstitution(subCallback);
  37:         }
  38:  
  39:         protected abstract void Render(HtmlTextWriter writer);
  40:  
  41:         private string RenderCallback(HttpContext context)
  42:         {
  43:             StringWriter baseWriter = new StringWriter(CultureInfo.CurrentCulture);
  44:             HtmlTextWriter writer = (HtmlTextWriter)_writerConstructor.Invoke(new object[] { baseWriter });
  45:             try
  46:             {
  47:                 _context = context;
  48:                 Render(writer);
  49:             }
  50:             finally
  51:             {
  52:                 _context = null;
  53:             }
  54:             return baseWriter.ToString();
  55:         }
  56:     }
  57: }

And then, I made the web part render the date/time using the call back method exposed off of this class.  Sample code below:

   1: using System;
   2: using System.Runtime.InteropServices;
   3: using System.Web.UI;
   4: using System.Web.UI.WebControls.WebParts;
   5: using System.Xml.Serialization;
   6:  
   7: using Microsoft.SharePoint;
   8: using Microsoft.SharePoint.WebControls;
   9: using Microsoft.SharePoint.WebPartPages;
  10:  
  11: namespace PCSWebPart
  12: {
  13:     [Guid("<random guid>")]
  14:     public class PCSWebPart : System.Web.UI.WebControls.WebParts.WebPart
  15:     {
  16:         public PCSWebPart()
  17:         {
  18:             this.ExportMode = WebPartExportMode.All;
  19:         }
  20:  
  21:         protected override void Render(HtmlTextWriter writer)
  22:         {
  23:             ShowTime st = new ShowTime();
  24:             st.Render(Context, writer);
  25:         }
  26:     }
  27:  
  28:     class ShowTime : DontCachePlease
  29:     {
  30:         public ShowTime()
  31:         {}
  32:  
  33:         protected override void Render(HtmlTextWriter writer)
  34:         { writer.WriteLine(DateTime.Now.ToString()); }
  35:     }   
  36: }

And that's it! I was able to see it working in the UI once I deployed this web part to my collaboration portal site.  Every time, I request for the page where I have test web parts loaded (1 using post caching substitution and the other without it), I could see the date/time value in the web part that does not use post caching substitution does not change till the cache expiration time I set, whereas the other show different date/time on every page request.  A visual representation of one my test instance below:

image

Hope this tip was helpful!!!

Wildcard Search in SharePoint

Being able to perform wildcard search is probably an area that's missing in out of the box search in SharePoint.  Here's a small web part sample you can use to perform wildcard search in MOSS 2007 (and it can be engineered backward to fit into SharePoint Portal Server 2003 as well).

The code in it is very crude, I agree :)  It's just a sample I wrote once and built upon multiple times for several different customer situations and so you might find commented out code snippets.  I have not put in any comments to explain what I am doing - sorry about that.  But I believe, a good developer's eye would catch-up on those code details very easily.

My other reasoning is: If it was so difficult for me to pull this code up, it should be that way for people who read this code :) (A traditional developer-related PJ I read somewhere...).

Nevertheless, try using the sample web part (oh yep, you need to have the latest WSS 3.0 Tools: Visual Studio 2005 Extensions, Version 1.1 installed to be able to open the enclosed VS solution) and let me know how it is.

Hopefully, it helps you as it helped many of my customers!!

"Failed to verify user permissions" error when using DspSts.asmx web service

I recently handled a case where the customer sees a "Failed to verify user permissions" error when accessing the DspSts.asmx web service in SharePoint 2007.  Below is the code similar to what they were using to invoke the Query method of DspSts.asmx web service.  It's also very similar to the same provided in SharePoint SDK.  Surprisingly, they saw this error only in their environment and we were unable to replicate it at our end.

The code:

   1:             string siteUrl = ConfigurationSettings.AppSettings["siteUrl"].ToString();
   2:             string username = ConfigurationSettings.AppSettings["username"].ToString();
   3:             string password = ConfigurationSettings.AppSettings["password"].ToString();
   4:             string domain = ConfigurationSettings.AppSettings["domain"].ToString();
   5:             dsptest.StsAdapter stsAdapter = new dsptest.StsAdapter();
   6:             stsAdapter.Url = siteUrl + "/_vti_bin/DspSts.asmx";
   7:             stsAdapter.Credentials = new NetworkCredential(username, password, domain);
   8:             string selectedList = comboBox1.SelectedItem.ToString();
   9:             string selectedListGuid = selectedList.Substring(selectedList.IndexOf("|")+2);
  10:             string[] vArray = new string[1];
  11:             vArray[0] = "1.0";
  12:             dsptest.Versions versions = new dsptest.Versions();
  13:             versions.version = vArray;
  14:             stsAdapter.versions = versions;
  15:             dsptest.RequestHeader reqHeader = new dsptest.RequestHeader();
  16:             reqHeader.document = dsptest.DocumentType.content;
  17:             reqHeader.method = dsptest.MethodType.query;
  18:             stsAdapter.request = reqHeader;
  19:             dsptest.QueryRequest myRequest = new dsptest.QueryRequest();
  20:             dsptest.DSQuery sQuery = new dsptest.DSQuery();
  21:             sQuery.select = "/list[@id='" + selectedListGuid + "']";
  22:             myRequest.dsQuery = sQuery;
  23:             dsptest.DspQuery spquery = new dsptest.DspQuery();
  24:             myRequest.dsQuery.Query = spquery;
  25:             try
  26:             {
  27:                 XmlNode xmlnode = stsAdapter.Query(myRequest);
  28:                 textBox1.Text = xmlnode.OuterXml;
  29:             }
  30:             catch (Exception _e)
  31:             {
  32:                 textBox1.Text = "Error: " + _e.Message + System.Environment.NewLine +
  33:                     "Stack Trace: " + System.Environment.NewLine +
  34:                     _e.StackTrace;
  35:             }

On investigating this issue, we found that they have enabled anonymous access at their web application level.  The problem is they had enabled anonymous access ONLY at their web application level.

The way to enable anonymous access at web application level is:

Browse to central administration site > application management > authentication providers (under application security)

image

Select the default provider where you wish to enable anonymous access (shown below)

image

And enabled anonymous access

image

When we do this, the web site at the IIS level will have its anonymous access enabled when we check the Directory Security tab from IIS MMC.  However, site collections within this web application are not yet configured to work with anonymous access.  Since the DspSts.asmx web service sits within the site collection, it looks like it has some problem understanding the call that's made from an anonymous contexts and it fails with the error:

   1:    Error: Failed to verify user permissions.
   2:    Stack Trace: 
   3:    at System.Web.Services.Protocols.SoapHttpClientProtocol.ReadResponse(SoapClientMessage message, WebResponse response, Stream responseStream, Boolean asyncCall)
   4:    at System.Web.Services.Protocols.SoapHttpClientProtocol.Invoke(String methodName, Object[] parameters)
   5:    at PPSIssueTester.dsptest.StsAdapter.Query(QueryRequest queryRequest) in C:\Documents and Settings\Administrator\Desktop\PPSIssueTester\PPSIssueTester\Web References\dsptest\Reference.cs:line 127
   6:    at PPSIssueTester.Form1.button2_Click(Object sender, EventArgs e) in C:\Documents and Settings\Administrator\Desktop\PPSIssueTester\PPSIssueTester\Form1.cs:line 110

These are the scenarios we've been testing:

1. NTLM authentication setup at both web application and site collections level with anonymous access enabled no where - call to DspSts.asmx works.

2. Anonymous access enabled at both web application and site collections level - call to DspSts.asmx works.

3. Anonymous access enabled only at web application and not at site collections - call to DspSts.asmx fails with the above error.

Well, the easiest resolution is to either enable anonymous access at site collections as well or to simply use NTML authentication (or form authentication) without enabling anonymous access.  It's another question as to whether having authentication configured this way is recommended or not (i.e., enabling anonymous access only at the web application level).  None of the SharePoint components fail by having authentication configured this way - per my understanding.  But apparently, this configuration seems to have some problem in terms of using DspSts.asmx web service.

The buzz with "UrlQueryString" in SharePoint navigation

SharePoint's navigation APIs have a property exposed that allows us to set query string properties for a URL.  Sadly, but not surprisingly, this only works when root site collection has publishing feature enabled.  Reason is simple...

One a WSS only OR on a site where publishing feature is not enabled, the property "UrlQueryString" is not available in the default property enumeration of the navigation node's property collection.  Run a simple code like the following

   1:                     SPNavigationNodeCollection quickLaunchNodes = web.Navigation.QuickLaunch;
   2:                     foreach (SPNavigationNode parentNode in quickLaunchNodes)
   3:                     {
   4:                         if (parentNode.Title.Equals("Lists"))
   5:                         {
   6:                             foreach (SPNavigationNode childNode in parentNode.Children)
   7:                             {
   8:                                 Hashtable ht = childNode.Properties;
   9:                                 foreach (string key in ht.Keys)
  10:                                 {
  11:                                     MessageBox.Show(key);
  12:                                 }                                
  13:                             }
  14:                             
  15:                         }
  16:                     }

And you'll see the property collection returned on a WSS only OR a site where publishing feature is not enabled returns only one property "vti_navsequencechild" and it's value will be set to "True".  Whereas when you enumerate this property bag on a publishing feature enabled site collection, you'll see at least 8 properties as show below

    Count = 8
    ["vti_navsequencechild"]: "true"
    ["CreatedDate"]: {7/24/2008 11:16:59 AM}
    ["UrlQueryString"]: ""
    ["LastModifiedDate"]: {7/24/2008 11:58:52 AM}
    ["Audience"]: ""
    ["Target"]: ""
    ["Description"]: ""
    ["UrlFragment"]: ""

"UrlQueryString" is available as one of the properties to be set and so will take effect when set with a query string parameter value.  On a site where publishing feature is not enabled, you'll still be able to set this property with a value, but there will be no effect in the UI.  Yep! it doesn't sound good, but that's the way it is :(

Check this article out!

Organization Hierarchy a Mess?

The "Organization Hierarchy" is a cool little thing that let's SharePoint users to remember who their colleagues are and more importantly who their manager is.

image

If you are wondering about customizing it, honestly, it's quite a bit of a work if you want to do that via features or using OM for the "small" issue in hand.

This pretty little control is actually marked up in the person.aspx page in the "SPSMSITEHOST" site definition.  The actual control reference is "SPSWC:ProfileManages".  Looks like this control is designed to lookup SharePoint's user profile database for filling itself up.  And quite obviously, if proper mapping (between managers/employees) is done in Active Directory that information is captured in SharePoint's user profile database as well and the accurate AD structure is replicated in SharePoint's user profile database.  This acts as the primary source for the "Organization Hierarchy" control.

There was a recent customer query, where they had ~ 12,000 user profiles in their Active Directory.  Worst, those user profiles did not have proper structure defined in AD i.e., no manager employee association or whatever!  They setup My Site and fact a performance issue and quite obviously the "Organization Hierarchy" control was causing it.

As I said earlier, "Organization Hierarchy" control has no intelligence of its own and purely relies on SharePoint's user profile module, which in turn relies on AD structure to populate information.  Now, since this customer did not have a structured AD, it ended up being a mess as all their user profiles (~ 12,000) was displayed in the "Organization Hierarchy" control.  Actually, the "Organization Hierarchy" control "tries" displaying all the data and eventually times out or if it displays, it does after a good 10 minutes delay.  So much for their unstructured AD.

Now, coming to the ways to fix this problem.

1. Delete all the user profiles you've loaded in your User Profile database.

2. Remove the "Organization Hierarchy" control using SPD.

The customer did not want to delete the user profiles, so the suggestion to remove the "Organization Hierarchy" control via SPD was the only one feasible.  All that the customer had to do was to login to open their http://<sharepoint>/person.aspx page using SPD.  Find the control and delete it as shown below:

image

Once this is done, it affect the person.aspx page of all users in that SharePoint Server Farm.  I couldn't think of any other means (for e.g., via features) because this control seems to be embedded in the person.aspx page as I told earlier.  There might be ways of achieving it via feature (may be looping through the controls collection and removing this particular control), but to me it seemed to be a over-shot for the simply issue in hand.

FBA and User Display Names in SharePoint 2007

If you have been working with FBA for a while, you might have noticed that the user name displayed in the Welcome control shows the account name of the FBA user.  This is because of the field the welcome control is mapped to.

image

As shown here, the welcome control simply points to the name property of the SPUser object.  One way of making it to display the full name is to import User Profile from your FBA data source and wait for the timer service job to complete to see the full name displayed in the Welcome control.  I've explained the other way below.

This was a very recent request where the user wanted to dynamically change the display name the first time a user logs into the FBA authentication enabled site.  It's very simple to achieve, but requires some careful consideration of things to get it to work perfectly!

In my below sample, I have configured FBA using LDAP provider.  This is kind of simple, because I can easily use DirectoryServices API to fetch user properties from AD.  I implemented this functionality in the form of a web part.  I also have a button and on clicking it I change the display name.  Here's the complete web part code:

using System;
using System.Runtime.InteropServices;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Xml.Serialization;
using System.DirectoryServices;
using System.IO;

using Microsoft.SharePoint;
using Microsoft.SharePoint.WebControls;
using Microsoft.SharePoint.WebPartPages;

namespace FBANameChanger
{
    [Guid("8d557e7e-a7ef-45e7-a91c-386faf6c0389")]
    public class ChangeFbaName : System.Web.UI.WebControls.WebParts.WebPart
    {
        public ChangeFbaName()
        {
        }

        Button btn = null;

        protected override void CreateChildControls()
        {
            btn = new Button();
            btn.Text = "Click to change...";
            btn.Click +=new EventHandler(btn_Click);
            Controls.Add(btn);
        }

        public override void RenderControl(HtmlTextWriter writer)
        {
            writer.Write("<table><tr><td>");
            btn.RenderControl(writer);
            writer.Write("</td></tr></table>");
        }

        private static string ReturnADEmail(string accntName)
        {
            string email = string.Empty;
            DirectoryEntry de = null;
            DirectorySearcher ds = null;
            try
            {
                de = new DirectoryEntry("", "sr", "sr", AuthenticationTypes.Secure);
                ds = new DirectorySearcher(de);
                ds.Filter = string.Format("(sAMAccountName={0})", accntName);
                SearchResult sr = ds.FindOne();
                if (sr != null)
                {
                    de = sr.GetDirectoryEntry();
                    email = de.Properties["mail"].Value.ToString();
                }
                else
                {
                    // If nothing found handle that case here
                }
            }
            catch (Exception e)
            {
                // Exception handling mechanism
            }
            finally
            {
                if (ds != null)
                    ds.Dispose();
                if (de != null)
                    de.Dispose();
            }
            return email;
        }

        private static string ReturnADDisplayName(string accntName)
        {
            string fullName = string.Empty;
            DirectoryEntry de = null;
            DirectorySearcher ds = null;
            try
            {
                de = new DirectoryEntry("", "sr", "sr", AuthenticationTypes.Secure);
                ds = new DirectorySearcher(de);
                ds.Filter = string.Format("(sAMAccountName={0})", accntName);
                SearchResult sr = ds.FindOne();
                if (sr != null)
                {
                    de = sr.GetDirectoryEntry();
                    fullName = de.Properties["cn"].Value.ToString();
                }
                else
                {
                    // If nothing found handle that case here
                }
            }
            catch (Exception e)
            {
                // Exception handling mechanism
            }
            finally
            {
                if (ds != null)
                    ds.Dispose();
                if (de != null)
                    de.Dispose();
            }
            return fullName;
        }        

        private void btn_Click(object sender, EventArgs e)
        {
            SPSecurity.RunWithElevatedPrivileges(delegate()
            {
                string identity = Page.User.Identity.Name;
                string loginName = "mossdcLdapMembers:" + identity;
                string displayName = ReturnADDisplayName(identity);
                string email = ReturnADEmail(identity);
                SPWeb web = SPContext.Current.Web;
                web.AllowUnsafeUpdates = true;
                try
                {
                    SPUser user = web.AllUsers[loginName];
                    user.Name = displayName;
                    user.Email = email;
                    user.Update();
                    Page.Response.Redirect(web.Site.Url.ToString());
                }
                catch (UnauthorizedAccessException _unauthorized)
                { Page.Response.Redirect(web.Site.Url.ToString()); }
                catch (Exception _e)
                { }
            });
        }
    }
}

Most of the code snippet is self-explanatory.  However, I want to highlight on some parts where you might ask "Why?".

                try
                {
                    SPUser user = web.AllUsers[loginName];
                    user.Name = displayName;
                    user.Email = email;
                    user.Update();
                    Page.Response.Redirect(web.Site.Url.ToString());
                }
                catch (UnauthorizedAccessException _unauthorized)
                { Page.Response.Redirect(web.Site.Url.ToString()); }

In the above code snippet, I've used explicit redirection in the "try" block because it requires a refresh to show the modified user full name.  Instead of using VB/Java Script functions, I simply redirect the control to the home page.  The "catch" block, catches the "UnauthorizedAccessException".  I have noticed that whenever the user context has read only permissions on the site, the Update method call on SPUser object throws an "Access Denied" exception.  I tried a lot to debug this to see why this happens, but couldn't.  So, as a work-around I simply redirect.

Strangely, even though the user.Update() call in the above code throws an unauthorized exception, the call actually succeeds.

Below screenshot shows the deployed web part I used.

image

Once I click the "Click to change..." button, I see the modified full name of the current user that's logged in.  A screenshot of that below:

image

If you are using your own repository for user account (like a database etc.,).  You'll simply need to modify the logic to retrieve the respective values back from your repository.  In the above code, I've simply modified two properties, the display name and the email address.  You can modify other properties in a similar manner.

A more comprehensive approach could be to write a class library that implements methods to return the properties you need and using it in your web part.

I hope this post was helpful!

Why are my SharePoint Groups drop-down disabled?

This was an interesting issue.  The problem a customer reported was that when they wanted to add a user to a SharePoint Group, the "Add users to a SharePoint group" drop-down was disabled.  On investigation, it was found the customer was creating a web application and a site collection "on-the-fly", through SharePoint object model code.  They were also creating their custom SharePoint groups that were derived from default groups.  However, they are not able to assign a user to their groups.

If you are wondering what I am talking about in the above paragraph, here's a screenshot that I believe will be more descriptive of the problem:

image

The red rectangle is the one to note.  That's the drop-down, which was not available for administrators to select and was grayed out!  The customer was doing everything right here.  Create web application, create a top-level site collection and create SharePoint groups (there were few instances where customers had a misconception that creating a web application/site collection via object model code would create the default groups automatically - well, that's not actually the case.  I mean it would be good to have that feature, but sadly it isn't there.  So, we have to create the user groups manually via code), every step is appropriate.

On further investigation, I noticed that the "AssociatedGroup" property was not defined in customer's code.  Below is the code snippet that corrected the problem.

image

Again the red rectangle above, shows how to utilize the "AssociatedGroup" property.  Setting this property as shown resolved this issue for the customer and they were able to assign users to SharePoint Groups.

What happened to my My Links Web Part?

Well, did you add a link like "file://C:/folder1/folder2"?  In one customer's environment this was the exact issue.  A user added some file system link like the above and ended up with the following symptoms:

1. The user couldn't add the "My Links" web part to his/her MySite.  Whenever he/she tries, it results in an error as shown below

image

This apparently becomes very bad, as he/she would not be able to access his/her MySite after that (unless they remove the "My Links" web part from the page).

2. And when they browse to http://sharepointmysitehost/_layouts/MyQuickLinks.aspx (that's available in the quick launch menu), they get to see a blank list as shown below

image

Bad for them a mistake as silly as using a "/" instead of a "\" causes them problems.  Worst, if they had 100 good links and 1 bad link - everything will go for a toss!

When we had a look at the UserLinks table in the SharedServices DB, we could see the bad link formatted like what is shown below

image

Surprisingly, links with "/" that's pointing to a file does not exhibit this issue.  Further, links that are added with a "file:///" instead of "file://" also does not exhibit this issue.

Well, if we simple delete it off - it fixes the issue, but modifying anything in SharePoint database (even if it's something as simple as the above) is not supported.  Having said that, SharePoint object model comes for our rescue.  Below is the small piece of code we used to get customer's "My Links" back in shape, though with the culprit deleted, which I think they won't mind adding again (correctly that is).

So, the code that brought this customer out of this scenario is:

            try
            {
                int count = 0;
                string personalSiteUrl = tbMySiteUrl.Text.ToLower();
                string personalSiteOwner = tbOwner.Text.ToLower();
                string fixer = string.Empty;
                using (SPSite oSite = new SPSite(personalSiteUrl))
                {
                    ServerContext sc = ServerContext.GetContext(oSite);
                    UserProfileManager upm = new UserProfileManager(sc);
                    UserProfile up = upm.GetUserProfile(personalSiteOwner);
                    QuickLinkManager qlm = up.QuickLinks;
                    foreach (QuickLink ql in qlm.GetItems())
                    {
                        try
                        {
                            fixer = ql.Url.ToString();
                        }
                        catch (System.UriFormatException uri)
                        {
                            ql.Delete();
                            count++;
                        }
                    }
                }
                MessageBox.Show("Deleted a total of " + count.ToString() + " bad link(s)");
            }
            catch (Exception _e)
            {
                MessageBox.Show("Error: " + _e.Message + System.Environment.NewLine + System.Environment.NewLine +
                    "Stack Trace: " + _e.StackTrace);
            }

It's a windows application with 2 text boxes.  1 that takes the MySite URL as its value and the other takes the MySite owner's account as input.  It also has a button - clicking that does the magic of deleting the nasty little link out.  Oh yes! you should add a reference to the following namespaces though:

using Microsoft.SharePoint;
using Microsoft.Office.Server;
using Microsoft.Office.Server.UserProfiles;
Not always people will end up in the above scenario, but if "fate" frowns on you and you get into the above mess, I hope this post will come handy for you!

What permissions are behind the permission levels (roles) in SharePoint

Recently, I was involved in a support request where I had to find out what SPBasePermissions are assigned behind permission level in SharePoint using SharePoint OM code.  First some basics about permission level and base permissions.

If you are in your SharePoint site, click Site Actions > Site Settings > click "People and groups" under "Users and Permissions" section > click "Site Permissions" in the left navigation menu > And use the Settings menu in the Permissions list to select "Permission Levels".  You'll get to see the roles (technically these are permissions levels).

image

If you click on one particular role (for e.g., Contribute), you'll get to see the "Permissions" assigned to that particular role.

image

These are basically classified into List, Site & Personal permissions.  They basically dictate what action an user in a particular role can perform in the SharePoint site.  The permissions levels act as masks (permission masks to be precise) and allows us to group a set of base permissions within a sort of a group called "Permission Levels".

Now, the requirement I had was to find out which SharePoint role (e.g., contributor, designer etc.,) has which base permissions assigned to it.  The code below did it for me:

            StringBuilder sb = new StringBuilder();
            using (SPSite site = new SPSite("http://wss"))
            {
                using (SPWeb web = site.OpenWeb())
                {
                    SPRoleDefinitionCollection roleDefinitions = web.RoleDefinitions;
                    foreach (SPRoleDefinition roleDefinition in roleDefinitions)
                    {
                        sb.Append(System.Environment.NewLine + System.Environment.NewLine +
                            "Role Definition: " + roleDefinition.Name + System.Environment.NewLine +
                            "==================================================" +
                            System.Environment.NewLine);
                        XmlDocument xmldoc = new XmlDocument();
                        xmldoc.LoadXml(roleDefinition.Xml);
                        XmlNode nodes = xmldoc.DocumentElement;
                        sb.Append(nodes.Attributes["BasePermissions"].Value);
                    }
                    textBox1.Text = sb.ToString();
                }
            }

Here's the output:

Role Definition: Full Control
==================================================
FullMask

Role Definition: Design
==================================================
ViewListItems, AddListItems, EditListItems, DeleteListItems, ApproveItems, OpenItems, ViewVersions, DeleteVersions, CancelCheckout, ManagePersonalViews, ManageLists, ViewFormPages, Open, ViewPages, AddAndCustomizePages, ApplyThemeAndBorder, ApplyStyleSheets, CreateSSCSite, BrowseDirectories, BrowseUserInfo, AddDelPrivateWebParts, UpdatePersonalWebParts, UseClientIntegration, UseRemoteAPIs, CreateAlerts, EditMyUserInfo

Role Definition: Manage Hierarchy
==================================================
ViewListItems, AddListItems, EditListItems, DeleteListItems, OpenItems, ViewVersions, DeleteVersions, CancelCheckout, ManagePersonalViews, ManageLists, ViewFormPages, Open, ViewPages, AddAndCustomizePages, ViewUsageData, CreateSSCSite, ManageSubwebs, ManagePermissions, BrowseDirectories, BrowseUserInfo, AddDelPrivateWebParts, UpdatePersonalWebParts, ManageWeb, UseClientIntegration, UseRemoteAPIs, ManageAlerts, CreateAlerts, EditMyUserInfo, EnumeratePermissions

Role Definition: Approve
==================================================
ViewListItems, AddListItems, EditListItems, DeleteListItems, ApproveItems, OpenItems, ViewVersions, DeleteVersions, CancelCheckout, ManagePersonalViews, ViewFormPages, Open, ViewPages, CreateSSCSite, BrowseDirectories, BrowseUserInfo, AddDelPrivateWebParts, UpdatePersonalWebParts, UseClientIntegration, UseRemoteAPIs, CreateAlerts, EditMyUserInfo

Role Definition: Contribute
==================================================
ViewListItems, AddListItems, EditListItems, DeleteListItems, OpenItems, ViewVersions, DeleteVersions, ManagePersonalViews, ViewFormPages, Open, ViewPages, CreateSSCSite, BrowseDirectories, BrowseUserInfo, AddDelPrivateWebParts, UpdatePersonalWebParts, UseClientIntegration, UseRemoteAPIs, CreateAlerts, EditMyUserInfo

Role Definition: Read
==================================================
ViewListItems, OpenItems, ViewVersions, ViewFormPages, Open, ViewPages, CreateSSCSite, BrowseUserInfo, UseClientIntegration, UseRemoteAPIs, CreateAlerts

Role Definition: Restricted Read
==================================================
ViewListItems, OpenItems, Open, ViewPages

Role Definition: Limited Access
==================================================
ViewFormPages, Open, BrowseUserInfo, UseClientIntegration, UseRemoteAPIs

Role Definition: Sridhar Role
==================================================
9223372036854644735

Role Definition: View Only
==================================================
ViewListItems, ViewVersions, ViewFormPages, Open, ViewPages, CreateSSCSite, BrowseUserInfo, UseClientIntegration, UseRemoteAPIs, CreateAlerts

 

In situations where you aren't very sure if a particular base permission is assigned to a role or not, the above code snippet could prove handy!  SDK reference for SPRoleDefinition.BasePermissions property.

Indexing MSCMS 2001 using MOSS 2007 indexer?

This might be a cake-walk for most of you, but I got delayed a bit to get MSCMS 2001 contents indexed using MOSS 2007.  Basically what I saw after I setup a content source and started a full crawl was the below warning:

The specified address was excluded from the index. The crawl rules may have to be modified to include this address. (The item was deleted because it was either not found or the crawler was denied access to it.)

I verified the content access account specified in MOSS is also a CMS 2001 administrator.  And I can browse to the default channel in CMS 2001 from the MOSS server.  But yet, the above error!  After trial-n-error, I was able to figure out that the anonymous access that's enabled in CMS 2001 server was causing this issue.

With anonymous access enabled at MSCMS 2001 web site: when I browse to the CMS 2001 site via IE, I see a prompt to provide my domain credentials.  This essentially means that even though anonymous access is enabled CMS content themselves is not anonymous and that it has to look for windows credentials to authenticate the user.

With anonymous access disabled at MSCMS 2001 web site: when I browse to the CMS 2001 site via IE, I get to see the home page under default channel directly.  I login using a domain account that's a CMS 2001 administrator.

What I think is happening here is MOSS requests the URL provided and supplies the content access account, which is going to be a domain credential.  When this request hits the IIS web site, IIS - as the first guy to shake-hands, compares the credentials with anonymous user and tells MOSS indexer that it could not serve the request.

That's it! I simply removed anonymous access from the CMS 2001 web site and MOSS indexer started to index the content.  So, it's probably a good idea to make sure that anonymous access is switched off, when indexing CMS 2001 using MOSS 2007.  Cheers!

Project to customize the small search control in SharePoint 2007

The small search control that is rendered in the default SharePoint pages is a delegate control rendered through master page.  You can find it defined in the master page as follows: image

<asp:ContentPlaceHolder id="PlaceHolderSearchArea" runat="server">
    <SharePoint:DelegateControl runat="server" ControlId="SmallSearchInputBox" />
</asp:ContentPlaceHolder>

This control is actually available through a feature called "OSearchBasicFeature" located under 12\TEMPLATE\FEATURES.  In the feature elements file (in this feature it's called "SearchArea.xml"), you can see the following:

<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Control 
        Id="SmallSearchInputBox" 
        Sequence="50"
        ControlClass="Microsoft.SharePoint.Portal.WebControls.SearchBoxEx" 
        ControlAssembly="Microsoft.SharePoint.Portal, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c">
    <Property Name="GoImageUrl">/_layouts/images/gosearch.gif</Property>
    <Property Name="GoImageUrlRTL">/_layouts/images/goRTL.gif</Property>
    <Property Name="GoImageActiveUrl">/_layouts/images/gosearch.gif</Property>
    <Property Name="GoImageActiveUrlRTL">/_layouts/images/goRTL.gif</Property>
    <Property Name="DropDownMode">ShowDD</Property>
        <Property Name="SearchResultPageURL">/_layouts/osssearchresults.aspx</Property>
    <Property Name="ScopeDisplayGroupName"></Property>
    <Property Name="FrameType">None</Property>
    </Control>    
</Elements>

The main class here seems to be Microsoft.SharePoint.Portal.WebControls.SearchBoxEx.  To customize small search, you need to inherit from "SearchBoxEx" class and override "CreateChildControls" method to get your custom small search rendered.  Do not modify any out of the box files as they might lead you into an unsupported scenario.  Instead, following are the simple steps to get the small search control customized:

  1. Create a custom master page by copying the default.master page.  I haven't tested with minimal master as explained here but I am guessing that would work either.
  2. Copy/Paste "OSearchBasicFeature" in the same feature folder with a different name (like mySimpleSearch).
  3. Change the feature ID in the feature.xml file within mySimplerSearch feature.
  4. Change the control ID in the SearchArea.xml file to something like "mySimleSearchcontrol".
  5. Change the class & assembly referenced in the SearchArea.xml file to the control library you created above.
  6. Install & Activate this feature,
  7. Render the custom control you created in the custom master page have and remove "SmallSearchInputBox" control from the master page.

This should do the tricky and you should be able to literally customizing anything within the small search control.

Happy customizing your SharePoint environment - and I really hope this post was helpful!  Look around for things like this in SharePoint as there are plenty of "hacks" to get things done!!!

Create and Publish web pages in Publishing SharePoint sites programmatically

Microsoft.SharePoint.Publishing is the assembly that we need to use to get pages created and published.  A sample is provided below:

using (SPSite site = new SPSite("http://moss"))
{
    using (SPWeb web = site.OpenWeb())
    {
        PublishingSite pSite = new PublishingSite(site);
        SPContentType ctype = pSite.ContentTypes["Welcome Page"];
        PageLayoutCollection pageLayouts = pSite.GetPageLayouts(ctype, true);
        PageLayout pageLayout = pageLayouts["/_catalogs/masterpage/welcomesplash.aspx"];
        PublishingWeb pWeb = PublishingWeb.GetPublishingWeb(web);
        PublishingPageCollection pPages = pWeb.GetPublishingPages();
        PublishingPage pPage = pPages.Add("Programmatic_Test.aspx", pageLayout);
        SPListItem newpage = pPage.ListItem;
        newpage["Title"] = "Page added programmatically";
        newpage.Update();

        newpage.File.CheckIn("all looks good");
        newpage.File.Publish("all looks good");
    }
}
This is an easy way of automating creating & publishing pages in collaboration/publishing sites, especially when the need is to create/publish them at a larger scale.  I hope this tip is helpful!

"The site is not assigned to an indexer" error in SharePoint custom search

Strangely enough! We'll not see this issue with an OOTB search.  It is seen only when we use search APIs or web service and perform a custom search.

Apparently, when we use Microsoft.Office.Server.Search, the content database also gets indexed and this content database should be assigned to the particular site we are searching against.

The way you do this is by going to SharePoint Central Administration site > Application Management > Content Databases > click the content database in the list.

image

And make sure the Search Server is assigned.

image

More Posts Next page »
 
Page view tracker