Yesterday, I talked about an issue with the SearchBox Web Part resulting from not having the Scope Display Group it used defined on my site collection. I mentioned, the best way to fix this was to have your feature create the scope display group for you, so here is how its done. To accomplish this, you need to start by getting a SearchContext object to get access to the Scopes object which is used to manipulate anything related to scopes. Once you have an instance of the Scopes object the AllDisplayGroups property returns a ScopeDisplayGroupCollection. It is this collection that you can use to create a scope display group. Unfortunately, however it does not contain anything to find an existing group other than an integer indexer. This means your choices for determining if the scope already exists are a for loop or a try/catch around the Create method. I am not sure why the SharePoint API team decided to make it so difficult to determine if an item exists in a collection. Here is what the code looks like.
// need the collection to modify scope settings
using (SPSite currentSiteCollection = new SPSite(""))
{
// get a search context and get access to the scope manager
SearchContext searchContext = SearchContext.GetContext(currentSiteCollection);
Scopes scopes = new Scopes(searchContext);
// get all scope display groups
ScopeDisplayGroupCollection scopeDisplayGroups = scopes.AllDisplayGroups;
// create the scope display group and add scopes to it
ScopeDisplayGroup scopeDisplayGroup = scopeDisplayGroups.Create("My Scope Group", "Scope Group Description",
new Uri(currentSiteCollection.Url), true);
scopeDisplayGroup.Add(scopes.GetSharedScope("My Shared Scope 1"));
scopeDisplayGroup.Add(scopes.GetSharedScope("My Shared Scope 2"));
// update the scope
scopeDisplayGroup.Update();
}
When you create the new scope, use the GetSharedScope method of the Scopes object to get the shared scopes that you want to add to the display group. The true parameter on the Create method indicates if you want to show the group in the Admin UI or not. Once you have made the changes, be sure and call the Update method.
I have caused this to happen a number of times and the answer isn't obvious at first, so I thought I would post on it and explain what causes it and how to fix it. The behavior usually occurs for me when I am deploying a SearchBoxEx web part via a feature using an elements.xml file. What you will see is the web part gets deployed fine, but when you try to submit a query, it doesn't do anything and you get the following JavaScript error.
'options' is null or not an object.
The reason this happens for me is because in my configuration file I am setting the scope dropdown mode to Show, do not include contextual scopes and I have specified a ScopeDisplayGroupName that does not exist yet in my site collection. Actually any setting for the scope dropdown that displays a dropdown will cause this behavior when the scope display group does not exist. To fix it quickly, you can either change the scope dropdown mode or create the scope display group on your site collection. However, the correct way for me to fix this would be to include something in my feature receiver that creates the scope display group.
Well, if you read, the Microsoft SharePoint Products and Technologies Team Blog, you know that the SharePoint SDK and MOSS SDK have been updated to version 1.3 which includes updates for SP1. Instead of someone that just posts the fact that this came out, I am posting a useful tip that might affect those in a corporate environment. As you know, many companies like to lock down desktops of their users using a Group Policy. I find this funny, when they do it to developers. They don't want you to be able to install software, yet they will give you a tool such as Visual Studio to write software. I find it even more amusing when they lock down the Add/Remove Programs applet in the Control Panel. This accomplishes absolutely nothing.
Anyhow, not to get caught on that aside. Today, I was locked out of removing the previous version of the SDK, so I decided to circumvent the problem using msiexec.exe and determining the GUID of the installer. The way I determined the GUID was by looking at a log file it put in my temp folder. Then I just executed the following command and I was able to remove the SDK and install the new one.
msiexec /x {90120000-1121-0409-0000-0000000FF1CE}
Notice the 31i73 spelling of office in that GUID? Apparently someone at Microsoft has a sense of humor. This technique would work if you just installed the WSS SDK as well, but I am afraid I don't have that GUID, so you would have to look it up.
I have seen quite a few searches for this warning in our stats, so I thought I would address the following message received when importing an application definition into the Business Data Catalog.
Could not create profile page for Entity MyEntity. The error is: Default action exists for application 'MyInstance', entity 'MyEntity'. Profile page creation skipped.
When you import your application definition, this kind of looks like an error but in fact is not. This is just the message MOSS gives when you add a default action to an entity. I describe how to do that in the linked blog post. Unfortunately, this appears as an error in the Event Log instead of a warning. This is unfortunate, because your MOSS administrator will likely ask you about it (my last one did). The long story short. Don't worry about this message. It is perfectly normal to receive it when you have specified a default action.
I'll be giving my talk on Searching Business Data with MOSS Enterprise Search at the Tulsa SharePoint Developer's User Group on March 10th. This is the same presentation I gave at TechFest, but I have updated it some and will likely also cover indexing other sources as well as documents. It's at OSU-Tulsa NH153 at 6:00 PM. I'll post more info as it gets closer.
This unfortunately is not as simple as it should be. Out of the box, there is not an option to delete a single crawled property. I sort of understand the reason behind this, but when developing its very easy to get your crawled properties filled with a bunch of garbage as you are trying things out. It would be nice to clean that up. So how do you do it? Your only option is to make use of a setting on each category of crawled properties (i.e.: SharePoint, Business Data, People), labeled Delete all unmapped crawled properties. You can get there by clicking the Edit Category link after choosing a Crawled Property. To make use of this, you would need to remove any mapping of crawled properties you don't want to keep first. I don't really like this as an option all that much, especially when you are removing things from the SharePoint category. Unfortunately, this appears to be the only way to do it other than going directly to the database (which obviously is not recommended).
Examining the API, there is nothing in there to delete a single crawled property either. However, you can also use the API to do the above mentioned step if you want. Include a reference to Microsoft.SharePoint and Microsoft.Office.Server.Search.Administration. Here is the code to delete the unmapped properties.
// the schema class actually represents managed and crawled proeperties
Schema schema = new Schema(SearchContext.GetContext(currentSiteCollection));
// get the sharepoint category
Category category = schema.AllCategories("SharePoint");
// delete all unmapped properties
category.DeleteUnmappedProperties();
// after delete an update is required
category.Update();
Just a word of warning as I have fallen for this one a time or two and I should know better. Recently, I had a need to change the name of my site columns which meant of course I had to update my content types that were using it. After, I did this, I discovered, that my ItemEventReceivers were no longer firing. It was certainly strange behavior. Then I remembered, I needed to reset IIS (or cycle the app pool or whatever), to drop the cache on the content type before reactivating the feature with the changes in it.
Kind of a weird tip here, but I have ran into a situation where I have needed to do this. Basically, I have had an existing class library (for SharePoint development) and for some reason, I have decided I need to add ASP.NET file types to it (i.e. User Controls, Master Pages, etc.). A class library doesn't have these file types in the Add Item menu so you need a Web Application Project. When it comes down to it a Web Application Project really is the same thing as a class library, it just has some extra settings. So when it comes down to it, you would either have to recreate the project and add your existing files back into it or just hack the XML of your .csproj file. I obviously prefer the latter.
To do this, start by Unloading the Project by right clicking your project and choosing Unload Project. Next Right click on your unloaded project and choose Edit <PrjectName>.csproj. Paste the line below in the first ProjectGroup element. Usually Visual Studio puts it underneath the ProjectGuid element.
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
That is the only change that is required. There are some optional settings that you can put in regarding the configuration of Cassini, but none of them are required to get you going. You can always configure them in Visual Studio. Once you have made the change, save the .csproj file and Reload Project. Once the project is loaded, all of the ASP.NET file types will be present in your Add Item menu. When you compile, it will still compile everything down to a single DLL and you can deploy it just as if it was a regular class library.
Today, I am continuing my series of posts on how to do basic tasks in SharePoint. Sometimes there is a need to activate a feature on a site collection or site using code. There are various reasons why you want to do this, but in my case, I needed to activate multiple features on multiple sites. The SPFeatureCollection object keeps track of which features are activated on a given site or site collection (not all features available for activation). There are a number of ways to access it. You can use the Features property on a given SPWeb or SPSite object. You can also make use of the WebFeatures or SiteFeatures from SPContext.Current. To activate a feature, call the Add method and pass it the GUID of the feature. Be warned that it will throw an InvalidOperationException if the feature is already activated. As with all SharePoint collections, the only way to determine if it exists is to use the indexer and see if it throws an exception. Here is an example of activating a feature at the site level.
SPWeb currentSite = SPContext.Current.Web;
currentSite.Features.Add(new Guid("{043C4BDD-9745-441a-A9A7-0BCD9B910319}"));
If you don't know the GUID of the feature but you do know the name of the Feature, you have to the FeatureDefinitions collection off of the SPFarm object. Unfortunately, you have to iterate through the whole collection and look at the title to find the feature you need. There is an example of it in the SDK.
Deactivating a feature is just as simple (provided you know the GUID). In this case I am using the WebFeatures properties off of the current context.
SPContext
I know some might consider this a simple topic, but my goal is to help newcomers to SharePoint as much as possible.
Today's topic seems like a simple task and it really is if you take a brute force method of solving it, however I wish I knew of a better solution. When specifically working with the TopNavigationBar collection, the only way to access the content is using an integer indexer (again why can't SharePoint collection ever have a contains method?). There is neither a contains method nor is there an indexer that you can pass a title or URL into. This is unfortunate. So how do you determine if a node is there? Brute force. Iterate through the collection and look for something that matches the title and URL. In this case, assume currentSite is an SPWeb object of the current site.
public bool NavigationNodeExists(string title, string url)
&& (string.Compare(currentSite.Navigation.TopNavigationBar[i].Url, url, StringComparison.OrdinalIgnoreCase) == 0)
I have not found a better way to do this. Unfortunately, this looks like such a noob solution, but I really can't think of anything better. Know a better way? Leave a comment please.
The other day I was reading Charlie Calvert's excellent post on LINQ and Deferred Execution and stumbled upon a logging feature inside LINQ to SQL. By setting the Log property to a TextWriter (in this case Console.Out), LINQ to SQL will log the query and what parameters it sent to the database server. Assign it before you execute your query like this.
MyDataContext.Log = Console.Out;
Obviously you don't have to log to the console, you can log to any text writer. This provides a great way to log what queries are going to the database and helps eliminate the mystery of what queries have been executing in your application. Obviously, you can view these queries in the debugger, but once your application is in production, this is a good solution.
I don't think there are enough complete examples on using the KeywordQuery class out there, so I am posting this today to help out. The example in the SDK is close, but not quite enough. The KeywordQuery class is used to execute a keyword syntax query against MOSS Enterprise Search. There is also a similar class that uses WSS search which basically works the same way. To use the KeywordQuery class start by passing it the path to your SSP in the constructor. This is a good place to use object initializers as I pointed out last week to set other needed properties.
// create a new KeywordQuery class, set the query and set to RelevantResults.
KeywordQuery myQuery = new KeywordQuery(siteCollection)
QueryText = string.Format("Color:\"{0}\"", "Red"),
ResultTypes = ResultType.RelevantResults
};
In this case I am doing a keyword query searching on the managed property Color with a value of red. You must set the ResultTypes property to RelevantResults in order to get search results back. To execute the query, use the Execute method. This method returns a ResultTableCollection which in turn contains a ResultTable for each type of Result (i.e.: RelevantResults). You can then load this into a datatable and do whatever with the data.
// execute the query and load the results into a datatable
ResultTableCollection queryResults = myQuery.Execute();
ResultTable queryResultsTable = queryResults[ResultType.RelevantResults];
DataTable queryDataTable = new DataTable();
queryDataTable.Load(queryResultsTable, LoadOption.OverwriteChanges);
Out of the box, this code will only return the default search properties such as Rank, Title, Author, Size, Path, Description, etc. If you want to return managed properties, make use of the SelectProperties string collection.
myQuery.SelectProperties.Add("Color");
myQuery.SelectProperties.Add("Size");
myQuery.SelectProperties.Add("Quantity");
Whenever I have worked with this class, I have discovered that adding anything to the SelectProperties collection will cause the default properties to no longer be returned. For example add the title and path properties back to the results with the following.
myQuery.SelectProperties.Add("Title");
myQuery.SelectProperties.Add("Path");
Lastly, using LINQ to DataSet you can further subquery your results if you needed to (i.e.: with a Quantity > 10).
var results = from queryResult in queryDataTable.AsEnumerable()
where queryResult.Field<int>("Quantity") > 10
select new
{
Title = queryResult.Field<string>("Title"),
Path = queryResult.Field<string>("Path"),
Size = queryResult.Field<string>("Size"),
Quantity = queryResult.Field<int>("Quantity")
};
Here is the complete code sample.
using (SPSite siteCollection = new SPSite(siteCollectionUrl))
// create a new KeywordQuery class, set the query and set to RelevantResults.
KeywordQuery myQuery = new KeywordQuery(siteCollection)
{
QueryText = string.Format("Color:\"{0}\"", "Red"),
ResultTypes = ResultType.RelevantResults
};
//
myQuery.SelectProperties.Add("Title");
myQuery.SelectProperties.Add("Path");
myQuery.SelectProperties.Add("Color");
myQuery.SelectProperties.Add("Size");
myQuery.SelectProperties.Add("Quantity");
// execute the query and load the results into a datatable
ResultTableCollection queryResults = myQuery.Execute();
ResultTable queryResultsTable = queryResults[ResultType.RelevantResults];
DataTable queryDataTable = new DataTable();
queryDataTable.Load(queryResultsTable, LoadOption.OverwriteChanges);
// query the results into a new anonymous type
var results = from queryResult in queryDataTable.AsEnumerable()
where queryResult.Field<int>("Quantity") > 10
select new
{
Title = queryResult.Field<string>("Title"),
Path = queryResult.Field<string>("Path"),
Size = queryResult.Field<string>("Size"),
Quantity = queryResult.Field<int>("Quantity")
};
Unfortunately, most of us aren't working in a perfect world, so it is bound to happen that you run into a dataset or two. Whatever the reason (the developer was lazy, you're maintaining legacy code, someone didn't know any better, or you're just working with the SharePoint API), it would be nice if there was a way to make dealing with datasets easier. LINQ to DataSet does this by allowing you to perform queries or move the data easily into a domain object.
To work with LINQ to DataSet, an extension method called AsEnumerable() is tacked onto the DataTable class making it queryable by LINQ This extension method is provided by adding System.Data.DataSetExtensions.dll to your project. Then, it is just a matter of knowing the syntax to get individual columns of data. Using a generic xtension method called Field, we can get the value of a column with the appropriate type.
In this example, we are going to filter the datatable on the ItemDateTime field and return a new anonymous type. We are assuming the datatable contains columns IntColumn1, StringColumn1, and ItemDateTime.
var queryResults = from queryResult in myDataTable.AsEnumerable()
where queryResult.Field<DateTime>("ItemDateTime") < DateTime.Now
select new
{
intColumn1 = queryResult.Field<int>("IntColumn1"),
stringColumn1 = queryResult.Field<string>("StringColun1")
};
This really makes it easy to perform subqueries on things presented to you via datasets. What if you want to get away from that nasty dataset and work with a domain object? LINQ makes that pretty easy as well. Just assign the values into a domain object. Here is a simple domain object (note: that it uses automatic properties).
public class MyDomainObject
public DateTime ItemDateTime
get;
set;
}
public int IntColumn1
public string StringColumn1
Here is how you would assign it to the domain object.
var queryResults2 = from queryResult in myDataTable.AsEnumerable()
select new MyDomainObject
{
ItemDateTime = queryResult.Field<DateTime>("ItemDateTime"),
IntColumn1 = queryResult.Field<int>("IntColumn1"),
StringColumn1 = queryResult.Field<string>("StringColun1")
};
Who knows how efficient this is, but it is quite simple. If you are curious what type queryResults2 is in this, it is an EnumerableRowCollection<MyDomainObject>. If you want it as a list, you can use the ToList() method, there is also ToArray() and ToDictionary() if that's what you want. That's all on LINQ and datasets for today. Hopefully, this will make your next experience with them better.
I have been using Enterprise Search lately to find sites programatically. The issue I ran into is that I needed to manipulate data on each site that was returned in the results and the only starting point I had was a fully qualified URL. At first, this task seems quite complicated because after examing the SPWeb object you will notice there is no constructor. I was hoping, I could just pass it the URL but alas that does not work. Really, the only way to create a SPWeb object is to use the SPSite object. So at first thought, I was like well how do I know what site collection something is in, given only its URL.
After a little digging in the documentation, it turns out when specifying a URL in the constructor of the SPSite object, you do not have to specify the path of the root of the site collection. You can specify the path to anything in the site collection and it will return an SPSite object representing the site collection you want. Consider the following example. I have a document at. In this case the site collection is at and the subsite is at. To get the SPWeb object, I start by passing the full URL of the document to the constructor of SPSite. I then just call OpenWeb with the default constructor to get the SPWeb object I need.
using (SPSite siteCollection = new SPSite(""))
SPWeb myWeb = siteCollection.OpenWeb();
It turns out that the functionality of OpenWeb differs greatly depending on what was passed into the constructor of SPSite. In this case, if you call OpenWeb with no parameters, it will return the SPWeb of the site where the document exists. For more information take a look at the URL below.
Object Initalizers aren't talked about very much as one of the new features in C# 3.0, but I find them pretty useful from time to time (esepcially when dealing with the SharePoint API). They are great, when there isn't a constructor that has all of the arguments that you need to initialize the class with in it. Here is an example of how I used it lately with the KeywordQuery class.
QueryText = string.Format("Color:\"{0}\"", myColor),
In this case I wanted to initialize the QueryText and ResultType properties. This makes the syntax much cleaner. Obviously this feature isn't life changing, but I find it a nice convenience.