Second Life of a Hungarian SharePoint Geek

April 24, 2013

Creating a mail distributor system using the incoming mail feature of SharePoint

Filed under: Incoming email, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 21:56

Wouldn’t it be great to implement your own custom logic to distribute mails to targeted addresses (for example, based on sender or subject of the mail) using SharePoint lists and event receivers? In this post I show you the fundamental technical issues and their solution to achieve that goal. My “custom logic” is quite simple, I send the same mail back to the sender, however you can build more sophisticated logic using the same technique, but more on that later.

Before starting Visual Studio, I’ve created a simple custom list called MailDistributor on my SharePoint site.

In Visual Studio I chose the Empty SharePoint Project template, and added a new List Email Event event receiver item.

Having these artifacts, I altered the default Elements.xml, to register the event receiver to the list I created earlier:

Code Snippet
  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3.     <Receivers ListUrl="Lists/MailDistributor">
  4.         <Receiver>
  5.             <Name>IncomingMailHandlerEmailReceived</Name>
  6.             <Type>EmailReceived</Type>
  7.             <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly>
  8.             <Class>MailDistributor.IncomingMailHandler</Class>
  9.             <SequenceNumber>10000</SequenceNumber>
  10.         </Receiver>
  11.     </Receivers>
  12. </Elements>

Regarding the code, the first step was to create the extension method GetMailMessage for the SPEmailMessage type to convert it to a MailMessage object (System.Net.Mail namespace). Fortunately, both of these object types have the same stream format in the background. This stream is directly accessible from the SPEmailMessage, however, MailMessage  is not creatable from the stream (or ByteArray / String) format. To solve this issue, I utilized the RxMailMessage type (copyright by Peter Huber, Singapore), that is a derived class of MailMessage with Stream and File support.

Code Snippet
  1. public static MailMessage GetMailMessage(this SPEmailMessage spEmailMessage)
  2. {
  3.     MailMessage result = null;
  4.     if (spEmailMessage != null)
  5.     {
  6.         result = RxMailMessage.CreateFromStream(spEmailMessage.GetMessageStream());
  7.     }
  8.  
  9.     return result;
  10. }

Below is the structure of the solution, highlighted the classes borrowed from Peter Huber.

image

The next challenge was to set the addressee (To property) of the MailMessage instance. Since there is no way to change this read-only property using the public methods of the type (in practice, you should set it already in the constructor), I had to apply my experience with Reflection, and set the private field to of the private field message of the MailMessage instance. Although I set only the To property in the example using the SetTo extension method, you could (and should!) set the Cc and Bcc fields as well. For example, clear these values to avoid perpetual sending / receiving the same message in the case the one of the Cc / Bcc fields contain the incoming mail address of the SharePoint list. To do that, you should implement the SetCc and SetBcc methods and from these methods call the SetMailAddressCollection method with the parameters “cc” and “bcc” accordingly.

Code Snippet
  1. public static void SetTo(this MailMessage mailMessage, MailAddressCollection mailAddressCollection)
  2. {
  3.     if ((mailMessage != null) && (mailAddressCollection != null))
  4.     {
  5.         SetMailAddressCollection(mailMessage, mailAddressCollection, "to");
  6.     }
  7. }
  8.  
  9. private static void SetMailAddressCollection(MailMessage mailMessage, MailAddressCollection mailAddressCollection, string fieldName)
  10. {
  11.     if ((mailMessage != null) && (mailAddressCollection != null) && (fieldName != null))
  12.     {
  13.         Type typeMailMessage = typeof(MailMessage);
  14.  
  15.         FieldInfo fi = typeMailMessage.GetField("message", BindingFlags.NonPublic | BindingFlags.Instance);
  16.  
  17.         if (fi != null)
  18.         {
  19.             object message = fi.GetValue(mailMessage);
  20.  
  21.             if (message != null)
  22.             {
  23.                 Type typeMessage = message.GetType(); // it is internal class System.Net.Mail.Message
  24.  
  25.                 FieldInfo fi2 = typeMessage.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
  26.  
  27.                 if (fi2 != null)
  28.                 {
  29.                     fi2.SetValue(message, mailAddressCollection);
  30.                 }
  31.             }
  32.         }
  33.     }
  34. }

In the event receiver, we convert the SPEmailMessage into a MailMessage instance, set its To property to the e-mail address of the original poster (From property of the MailMessage) and send the mail using an SmtpClient object. The Host property of the SmtpClient instance can be set using the Address of the configured SMTP server of the OutboundMailServiceInstance in the current web application.

As you can see, most of the code below is just tracing to help us to follow the process using DebugView. You are free to remove these lines without affecting the functionality, of course.

Code Snippet
  1. using System;
  2. using System.Diagnostics;
  3. using System.Net.Mail;
  4. using Microsoft.SharePoint;
  5. using Microsoft.SharePoint.Utilities;
  6.  
  7. namespace MailDistributor
  8. {
  9.     public class IncomingMailHandler : SPEmailEventReceiver
  10.     {
  11.         public override void EmailReceived(SPList list, SPEmailMessage emailMessage, string receiverData)
  12.         {
  13.             try
  14.             {
  15.                 Trace.TraceInformation("IncomingMailHandler starting…");
  16.  
  17.                 foreach (SPEmailHeader header in emailMessage.Headers)
  18.                 {
  19.                     Trace.TraceInformation("EmailReceived emailMessage header {0}, {1}", header.Name, header.Value);
  20.                 }
  21.  
  22.                 SmtpClient smtpClient = new SmtpClient();
  23.  
  24.                 smtpClient.Host = list.ParentWeb.Site.WebApplication.OutboundMailServiceInstance.Server.Address;
  25.  
  26.                 Trace.TraceInformation("IncomingMailHandler: getting mail message from stream");
  27.  
  28.                 MailMessage mailMessage = emailMessage.GetMailMessage();
  29.  
  30.                 Trace.TraceInformation("IncomingMailHandler: setting mail message To field");
  31.  
  32.                 mailMessage.SetTo(new MailAddressCollection { mailMessage.From });
  33.  
  34.                 Trace.TraceInformation("IncomingMailHandler: sending mail");
  35.  
  36.                 smtpClient.Send(mailMessage);
  37.                 
  38.                 Trace.TraceInformation("IncomingMailHandler: finished");
  39.             }
  40.             catch (Exception ex)
  41.             {
  42.                 Trace.TraceInformation("IncomingMailHandler exception: {0}", ex.Message);
  43.             }
  44.  
  45.         }
  46.  
  47.     }
  48.  
  49. }

After deploying the solution and activating the feature, we should enable the incoming mail for the MailDistributor list, set the mail address alias, and send a test message to this address. If there is no error, within about a minute we should receive the same mail, including formatting and attachments to the mailbox of the sender.

Using the Category settings of the mail to route the message

The rule we implemented is really a trivial one and has not much sense, but one can implement more complicated and more useful routing rules as well. My plan is to build a routing “engine” based on the Category settings of the mail.

As part of the Options / Tracking properties in Outlook, we can set not only Blue or Green categories, but our own custom categories (like SharePoint and Silverlight below) as well. As long as these categories are transferred within the mail, we can process them in our event receiver, look up SharePoint user profiles having the same values set in the Ask me about property, and route the message exactly to those users, implementing thus a simple but efficient knowledge management solution.

It would be even better if the user could choose those category values from a SharePoint Managed Metadata keyword list (a candidate for an Office 2013 mail-app?).

image

However there are some issues with the Category property that you should be aware of.

Based on this information, Outlook removes category settings from outgoing mails due to privacy concerns. One can alter this settings via registry (HKEY_CURRENT_USER\Software\Policies\Microsoft\Office\xx.0\Outlook\Preferences\SendPersonalCategories, where xx is the version number of Outlook, like 14 for Outlook 2010).

Exchange 2010 also removes the categories from the outgoing messages by default, as I’ve learned here. This behavior can be changes using the following PowerShell command:

Set-TransportConfig -ClearCategories:$False

A workaround for these issues might be an Outlook add-in, or a simply VBA code like this one, that illustrates, how to copy the mail categories to a custom mail header called X-Categories when sending the mail, thus avoiding losing of the categories:

Private Sub Application_ItemSend(ByVal item As Object, Cancel As Boolean)
    Dim mi As MailItem
    Set mi = item
    If Not mi Is Nothing Then
       item.PropertyAccessor.SetProperty "http://schemas.microsoft.com/mapi/string/{00020386-0000-0000-C000-000000000046}/X-Categories", item.Categories
    End If
End Sub

Of course, we should filter the mails affected by this behavior based on the To e-mail address, limiting it to the mails sent to our MailDistributor list.

October 26, 2012

How to populate the Attendees field of a SharePoint event based on the addressees of a meeting request? (Version 2)

In my previous post I already demonstrated a method to resolve the meeting attendees based on the mail addresses in the incoming mail, though – as I wrote there – that method has issues with event updates.

Note: In this post I show you an alternative, that – at least, based on my experience – performs better. However, the code below uses non-public API calls and accesses SharePoint database directly, so it is not a supported approach. Use this sample at you own risk and preferably only in test environments.

In this version of implementation we alter the standard pipeline of incoming mail processing for our calendar to inject our code into the process. To achieve that, we create an a SPEmailHandler that first invokes the ProcessMessage method of the SPCalendarEmailHandler class to achieve the standard functionality, then resolves the attendees using the To mail header property based on this technique, and updates the related item / all related items (in the case of a recurring event, it may be not a single item) in the list.

Note: In the case of the To mail header property we don’t need to unescape the value, so you should comment out this line of code in the GetUsersByMailTo method introduced in the first part of this post:

emailTo = emailTo.Replace("&lt;", "<").Replace("&gt;", ">");

We can get the corresponding item(s) based the unique identifier (UID, you can read details on Wikipedia) vCalendar property of the event using the GetCalendarProp method described in my former post. We use the GetExistingItems method below to get these related items:

  1. private SPListItemCollection GetExistingItems(SPList list, string uid)
  2. {            
  3.     SPQuery query = new SPQuery();
  4.     query.Query = "<Where><Eq><FieldRef Name=\"" + list.Fields[SPBuiltInFieldId.EmailCalendarUid].InternalName + "\"/><Value Type=\"Text\">" + SPEncode.HtmlEncode(uid) + "</Value></Eq></Where>";
  5.     SPListItemCollection existingItems = list.GetItems(query);
  6.     return existingItems;
  7. }

The following method allows us to invoke the ProcessMessage method of the SPCalendarEmailHandler class:

  1. private void ProcessMessage(SPList list, SPEmailMessage emailMessage)
  2. {
  3.     Trace.TraceInformation("Starting SPCalendarEmailHandler processing");
  4.  
  5.     string spCalendarEmailHandlerTypeName = "Microsoft.SharePoint.SPCalendarEmailHandler";
  6.  
  7.     // hack to get the Microsoft.SharPoint assembly
  8.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  9.     // and a reference to the type of the SPCalendarEmailHandler internal class
  10.     Type spCalendarEmailHandlerType = sharePointAssembly.GetType(spCalendarEmailHandlerTypeName);
  11.  
  12.     // spCalendarEmailHandler will be of type internal class
  13.     // Microsoft.SharePoint.SPCalendarEmailHandler
  14.     // defined in Microsoft.SharePoint assembly
  15.     object spCalendarEmailHandler = sharePointAssembly.CreateInstance(spCalendarEmailHandlerTypeName, false,
  16.         BindingFlags.Public | BindingFlags.Instance, null, new object[] { list }, CultureInfo.InvariantCulture, null);
  17.  
  18.     if (spCalendarEmailHandler != null)
  19.     {
  20.         MethodInfo mi_ProcessMessage = spCalendarEmailHandlerType.GetMethod("ProcessMessage",
  21.                     BindingFlags.Public | BindingFlags.Instance, null,
  22.                     new Type[] { typeof(SPEmailMessage) }, null
  23.                     );
  24.         if (mi_ProcessMessage != null)
  25.         {
  26.             // result of type SPEmailHandlerResult is ignored
  27.             mi_ProcessMessage.Invoke(spCalendarEmailHandler, new Object[] { emailMessage });
  28.         }
  29.     }
  30.  
  31.     Trace.TraceInformation("SPCalendarEmailHandler processing finished");
  32. }

Using these helper methods our EmailReceived method looks like these:

  1. public override void EmailReceived(SPList list, SPEmailMessage emailMessage, string receiverData)
  2. {
  3.     try
  4.     {
  5.         Trace.TraceInformation("EmailReceived started");
  6.  
  7.         string uid = GetCalendarProp(emailMessage, "UID");
  8.  
  9.         ProcessMessage(list, emailMessage);
  10.  
  11.         string emailTo = emailMessage.Headers["To"];
  12.         SPFieldUserValueCollection users = GetUsersByMailTo(list.ParentWeb, emailTo);
  13.  
  14.         if (!string.IsNullOrEmpty(uid))
  15.         {
  16.             SPListItemCollection existingItems = GetExistingItems(list, uid);
  17.             foreach (SPListItem listItem in existingItems)
  18.             {
  19.                 Trace.TraceInformation("Updating item ID: {0}, To: {1}", listItem.ID, emailTo);
  20.                 listItem[SPBuiltInFieldId.ParticipantsPicker] = users;
  21.                 listItem.Update();
  22.             }
  23.         }
  24.  
  25.         Trace.TraceInformation("EmailReceived calling base handler(s)…");
  26.     }
  27.     catch (Exception ex)
  28.     {
  29.         Trace.TraceInformation("EmailReceived exception: {0}", ex.Message);
  30.         Trace.TraceInformation(ex.StackTrace);
  31.     }
  32.     base.EmailReceived(list, emailMessage, receiverData);
  33. }

Finally, this method seems to fulfill our goals and resolves attendees both on new meeting requests and event updates.

October 15, 2012

How to list all SharePoint incoming mail aliases for a site or web application?

Filed under: Incoming email, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 21:21

In my recent samples I’ve illustrated how to check whether an alias is reserved and how to get details of mapping.

This time I provide you the code that helps to enumerate all aliases for a site or the entire web application. To use this code you need the EmailAliasRecord wrapper struct and the extension methods as introduced in the former post.

Note: the code below uses non-public API calls and accesses SharePoint database directly, so it is not a supported approach. Use this sample at you own risk and preferably only in test environments.

The information we need is accessible through the proc_EnumEmailAliases and proc_EnumEmailAliasesBySite stored procedure in the SharePoint content database of the give web application.

To access these procedures, I first introduced the ExecuteReader extension method:

  1. public static SqlDataReader ExecuteReader(this SPContentDatabase database, SqlCommand command, CommandBehavior behavior)
  2. {
  3.     SqlDataReader result = null;
  4.  
  5.     string sqlSessionTypeName = "Microsoft.SharePoint.Utilities.SqlSession";
  6.     Type spContentDatabaseType = typeof(SPContentDatabase);
  7.  
  8.     // hack to get the Microsoft.SharPoint assembly
  9.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  10.     // and a reference to the type of the SqlSession internal class
  11.     Type sqlSessionType = sharePointAssembly.GetType(sqlSessionTypeName);
  12.  
  13.     System.Reflection.PropertyInfo pi_SqlSession = spContentDatabaseType.GetProperty("SqlSession", BindingFlags.NonPublic | BindingFlags.Instance);
  14.  
  15.     if (pi_SqlSession != null)
  16.     {
  17.         // sqlSession will be of type internal class
  18.         // Microsoft.SharePoint.Utilities.SqlSession
  19.         // defined in Microsoft.SharePoint assembly
  20.         object sqlSession = pi_SqlSession.GetValue(database, null);
  21.  
  22.         MethodInfo mi_ExecuteReader = sqlSessionType.GetMethod("ExecuteReader", BindingFlags.Public | BindingFlags.Instance, null,
  23.                                             new Type[] { typeof(SqlCommand), typeof(CommandBehavior) }, null);
  24.  
  25.         if (mi_ExecuteReader != null)
  26.         {
  27.             result = mi_ExecuteReader.Invoke(sqlSession, new Object[] { command, behavior }) as SqlDataReader;
  28.         }
  29.     }
  30.  
  31.     return result;
  32. }

Having this method and the code from the previous post, the methods below can be used to display aliases mapped for lists in a specific site, in a content database or in a web application:

  1. private void DisplayEmailAliases(SPWebApplication webApp)
  2. {
  3.     foreach (SPContentDatabase database in webApp.ContentDatabases)
  4.     {
  5.         DisplayEmailAliases(database, null);
  6.     }
  7. }
  8.  
  9. private void DisplayEmailAliases(SPSite site)
  10. {
  11.     SPContentDatabase database = site.ContentDatabase;
  12.     DisplayEmailAliases(database, site.ID);
  13. }
  14.  
  15. private void DisplayEmailAliases(SPContentDatabase database, Guid? siteId)
  16. {
  17.     SqlCommand command;
  18.     List<EmailAliasRecord> list = new List<EmailAliasRecord>();
  19.     if (!siteId.HasValue)
  20.     {
  21.         command = new SqlCommand("proc_enumEmailAliases");
  22.     }
  23.     else
  24.     {
  25.         command = new SqlCommand("proc_enumEmailAliasesBySite");
  26.         command.Parameters.Add("@SiteId", SqlDbType.UniqueIdentifier).Value = siteId.Value;
  27.     }
  28.     command.CommandType = CommandType.StoredProcedure;
  29.  
  30.     using (SqlDataReader reader = database.ExecuteReader(command, CommandBehavior.CloseConnection))
  31.     {
  32.         while (reader.Read())
  33.         {
  34.             EmailAliasRecord ear = new EmailAliasRecord(reader);
  35.             Console.WriteLine(ear.ToString());
  36.         }
  37.     }
  38.  
  39. }

How to resolve SharePoint incoming mail alias mapping details?

Filed under: Incoming email, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 20:36

In my recent post I’ve illustrated how to check from code whether a given mail alias is already reserved. If you need to know details about the list / web the alias is mapped to, you have to work further on the issue.

Note: the code below uses non-public API calls and so it is not a supported approach. Use this sample at you own risk and preferably only in test environments.

First I introduced a few extension methods to make our live (and work with Reflection) a bit easier.

  1. public static class Extensions
  2. {
  3.     public static object GetPublicInstanceFieldValue(this Type type, string fieldName, object instance)
  4.     {
  5.         object result = null;
  6.         FieldInfo fi = type.GetField(fieldName);
  7.  
  8.         if ((fi != null) && (instance != null))
  9.         {
  10.             result = fi.GetValue(instance);
  11.         }
  12.  
  13.         return result;
  14.     }
  15.  
  16.     public static void SetPublicInstanceFieldValue(this Type type, string fieldName, object fieldValue, object instance)
  17.     {
  18.         FieldInfo fi = type.GetField(fieldName);
  19.  
  20.         if ((fi != null) && (instance != null))
  21.         {
  22.             fi.SetValue(instance, fieldValue);
  23.         }
  24.     }
  25.  
  26.     public static object GetPublicInstancePropertyValue(this Type type, string fieldName, object instance)
  27.     {
  28.         object result = null;
  29.         System.Reflection.PropertyInfo pi = type.GetProperty(fieldName);
  30.  
  31.         if ((pi != null) && (instance != null))
  32.         {
  33.             result = pi.GetValue(instance, null);
  34.         }
  35.  
  36.         return result;
  37.     }
  38. }

Next, I defined the following wrapper struct for the internal EmailAliasRecord struct (Microsoft.SharePoint.Administration namespace, Microsoft.SharePoint assembly):

  1. public struct EmailAliasRecord
  2. {
  3.     public string Alias { get; private set; }
  4.     public Guid ListId { get; private set; }
  5.     public Guid WebId { get; private set; }
  6.     public Guid SiteId { get; private set; }
  7.     public bool IsValid { get; private set; }
  8.  
  9.     private const string _emailAliasRecordTypeName = "Microsoft.SharePoint.Administration.EmailAliasRecord";
  10.  
  11.     public EmailAliasRecord(object emailAliasRecord) : this()
  12.     {
  13.         InitFields(emailAliasRecord);
  14.     }
  15.  
  16.     private void InitFields(object emailAliasRecord)
  17.     {
  18.         // hack to get the Microsoft.SharPoint assembly
  19.         Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  20.         // and a reference to the type of the EmailAliasRecord internal struct
  21.         Type emailAliasRecordType = sharePointAssembly.GetType(_emailAliasRecordTypeName);
  22.  
  23.         this.Alias = emailAliasRecordType.GetPublicInstanceFieldValue("alias", emailAliasRecord) as string;
  24.         this.ListId = (Guid)emailAliasRecordType.GetPublicInstanceFieldValue("listId", emailAliasRecord);
  25.         this.WebId = (Guid)emailAliasRecordType.GetPublicInstanceFieldValue("webId", emailAliasRecord);
  26.         this.SiteId = (Guid)emailAliasRecordType.GetPublicInstanceFieldValue("siteId", emailAliasRecord);
  27.         this.IsValid = (bool)emailAliasRecordType.GetPublicInstancePropertyValue("IsValid", emailAliasRecord);
  28.     }
  29.  
  30.     public EmailAliasRecord(SqlDataReader reader) : this()
  31.     {
  32.         object emailAliasRecord = null;
  33.  
  34.         // hack to get the Microsoft.SharPoint assembly
  35.         Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  36.         // and a reference to the type of the EmailAliasRecord internal struct
  37.         Type emailAliasRecordType = sharePointAssembly.GetType(_emailAliasRecordTypeName);
  38.  
  39.         emailAliasRecord = sharePointAssembly.CreateInstance(_emailAliasRecordTypeName);
  40.  
  41.         if (emailAliasRecord != null)
  42.         {
  43.             emailAliasRecordType.SetPublicInstanceFieldValue("alias", reader.GetString(0), emailAliasRecord);
  44.             emailAliasRecordType.SetPublicInstanceFieldValue("siteId", reader.GetGuid(1), emailAliasRecord);
  45.             emailAliasRecordType.SetPublicInstanceFieldValue("webId", reader.GetGuid(2), emailAliasRecord);
  46.             emailAliasRecordType.SetPublicInstanceFieldValue("listId", reader.GetGuid(3), emailAliasRecord);
  47.  
  48.             InitFields(emailAliasRecord);
  49.         }
  50.     }
  51.  
  52.     public override string ToString()
  53.     {
  54.         StringBuilder sb = new StringBuilder();
  55.  
  56.         using (SPSite site = new SPSite(this.SiteId))
  57.         {
  58.             using (SPWeb web = site.OpenWeb(this.WebId))
  59.             {
  60.                 SPList list = web.Lists[this.ListId];
  61.  
  62.                 sb.AppendFormat("{0} e-mail alias '{1}' found\r\n", this.IsValid ? "Valid" : "Invalid", this.Alias);
  63.  
  64.                 if (this.IsValid)
  65.                 {
  66.                     sb.Append("Mapped to:'\r\n");
  67.                     sb.AppendFormat("  Web title: {0}\r\n", web.Title);
  68.                     sb.AppendFormat("  Web URL: {0}\r\n", web.Url);
  69.                     sb.AppendFormat("  List title: {0}\r\n", list.Title);
  70.                 }
  71.             }
  72.         }
  73.  
  74.         return sb.ToString();
  75.     }
  76.     
  77. }

Using the code above, it is rather straightforward to call the private GetEmailAliasRecordFromDatabase method of the internal SPEmailMap class (namespace and assembly as above):

  1. private void GetEmailAliasRecordFromDatabase(string mailAlias)
  2. {
  3.     bool found = false;
  4.  
  5.     string spEmailMapTypeName = "Microsoft.SharePoint.Administration.SPEmailMap";
  6.     // hack to get the Microsoft.SharPoint assembly
  7.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  8.     // and a reference to the type of the SPElementProvider internal class
  9.     Type spEmailMapType = sharePointAssembly.GetType(spEmailMapTypeName);
  10.  
  11.  
  12.     // spEmailMap will be of type internal class
  13.     // Microsoft.SharePoint.Administration.SPEmailMap
  14.     // defined in Microsoft.SharePoint assembly
  15.     object spEmailMap = sharePointAssembly.CreateInstance(spEmailMapTypeName, false,
  16.         BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture, null);
  17.  
  18.     if (spEmailMap != null)
  19.     {
  20.         // we call
  21.         // internal EmailAliasRecord GetEmailAliasRecordFromDatabase(string alias)
  22.         MethodInfo mi_GetEmailAliasRecordFromDatabase = spEmailMapType.GetMethod("GetEmailAliasRecordFromDatabase",
  23.                 BindingFlags.NonPublic | BindingFlags.Instance, null,
  24.                 new Type[] { typeof(string) }, null
  25.                 );
  26.         if (mi_GetEmailAliasRecordFromDatabase != null)
  27.         {
  28.             object result = mi_GetEmailAliasRecordFromDatabase.Invoke(spEmailMap,
  29.                 new Object[] { mailAlias });
  30.  
  31.             EmailAliasRecord ear = new EmailAliasRecord(result);
  32.  
  33.             if (ear.IsValid)
  34.             {
  35.                 found = true;
  36.                 Console.WriteLine(ear.ToString());
  37.             }
  38.         }
  39.     }
  40.  
  41.     if (!found)
  42.     {
  43.         Console.WriteLine("Found no valid mapping for e-mail alias '{0}'", mailAlias);
  44.     }
  45. }

This code will output the Title property of the associated SPWeb and SPList objects, as well as the Url property of the SPWeb, thus helping you to find out where you set the specified mail alias.

October 9, 2012

Checking programmatically if an e-mail address is already in use for incoming mail

Filed under: Incoming email, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 21:53

Recently I work quite a lot with the SharePoint calendar and the incoming mail feature of SharePoint. During my experiments I found an interesting problem, namely how one can check from code if a specific e-mail address is already configured for any of the lists.

Note: Since the domain-part of the address – that is the part after the @ sign – is fixed as part of the System Settings at the Central Administration, we can only set the e-mail alias – the part before the @ sign – for the lists.

clip_image001

Without this capability, the only option is the trial and error method that means we try to assign the mail address from code, and call the Update method of SPList as illustrated below:

SPList list = web.Lists[listName];
list.EmailAlias = "MailAlias"; // the part of the e-mail address before the @ sign
list.Update();

In the case the alias is already reserved for another list, we receive an SPException (Unable to assign this e-mail address to the list, because the address is in use.) that we could optionally handle using a try/catch block.

As you may know using this kind of error handling structure for checking existing items does not perform well, so if there is a great chance for conflicting mail addresses, it would be far better (and faster) to make the check without exceptions.

Let’s see what happens when we try to save the list configuration either from the UI or by custom code.

First, the Update(bool bFromMigration) method of the SPList is called, that calls the AssignAlias(string alias, SPList list) method of the SPEmailMap class. It validates the format of the alias via the static ValidateAlias(string alias) method, and then check the existence of a list having this alias through the CanGetListFromDatabase(string alias) method. This method uses another method GetEmailAliasRecordFromDatabase(string alias) that access the SharePoint database and returns list information in the form of an EmailAliasRecord object.

Note: the e-mail addresses used for this kind of check are stored in the EmailEnabledLists table (with fields Alias, Deleted, SiteId, WebId, ListId) of the configuration database SharePoint. The stored procedure used to get the list data based on the alias is the proc_getEmailEnabledListByAlias.

In the following code, I illustrate how to call the CanGetListFromDatabase method to check the existence of an alias.

Note: the code below uses non-public API calls and so it is not a supported approach. Use this sample at you own risk and preferably only in test environments.

  1. private bool IsAliasInUse(string mailAlias)
  2. {
  3.     bool result = false;
  4.  
  5.     string spEmailMapTypeName = "Microsoft.SharePoint.Administration.SPEmailMap";
  6.     // hack to get the Microsoft.SharPoint assembly
  7.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  8.     // and a reference to the type of the SPEmailMap internal class
  9.     Type spEmailMapType = sharePointAssembly.GetType(spEmailMapTypeName);
  10.  
  11.  
  12.     // spEmailMap will be of type internal class
  13.     // Microsoft.SharePoint.Administration.SPEmailMap
  14.     // defined in Microsoft.SharePoint assembly
  15.     object spEmailMap = sharePointAssembly.CreateInstance(spEmailMapTypeName, false,
  16.         BindingFlags.Public | BindingFlags.Instance, null, null, CultureInfo.InvariantCulture, null);
  17.  
  18.     if (spEmailMap != null)
  19.     {
  20.         MethodInfo mi_CanGetListFromDatabase = spEmailMapType.GetMethod("CanGetListFromDatabase",
  21.                 BindingFlags.NonPublic | BindingFlags.Instance, null,
  22.                 new Type[] { typeof(string) }, null
  23.                 );
  24.         if (mi_CanGetListFromDatabase != null)
  25.         {
  26.             // result is bool
  27.             result = (bool)mi_CanGetListFromDatabase.Invoke(spEmailMap,
  28.                 new Object[] { mailAlias });
  29.  
  30.         }
  31.     }
  32.  
  33.     return result;
  34. }

May 13, 2011

‘User cannot be found’ error solved using the SharePoint object model

Filed under: Administration, Bugs, Reflection, SP 2010 — Tags: , , , — Peter Holpar @ 21:24

In my recent post I wrote about the ‘User cannot be found‘ error on the Change site collection administrators page and how to find the source of the issue using a SQL query.

In the current post I would like to provide you a bit more supported and more automated solution using the server side SharePoint API and some Reflectioning.

In this approach we first gets the list of all admin and content web applications, then iterate through the site collection of each web apps.

  1. private void CheckSiteAdminsOnFarm()
  2. {
  3.  
  4.     // we check both admin and content sites
  5.     List<SPWebApplication> appsToCheck = SPWebService.AdministrationService.WebApplications.Union(
  6.         SPWebService.ContentService.WebApplications).ToList();
  7.  
  8.     // we iterate through all site collections of all apps
  9.     appsToCheck.ForEach(webApp => webApp.Sites.ToList().ForEach(site => CheckSiteAdmins(site, @"SPMMX\administrator")));
  10.  
  11. }

You can see that we provide a login name to the CheckSiteAdmins method. Admin users that cannot be resolved as an existing user will be replaced with this user account.

In CheckSiteAdmins we first have to initialize the site using the private InitSite method of the SPSite class. It is necessary to call  InitSite, since it populates the value of the private m_OwnerID and m_nSecondaryContactID fields. Next we read the int values of these fields and try to resolve the to user through our GetUserName method.

  1. private void CheckSiteAdmins(SPSite site, String adminUser)
  2. {
  3.     try
  4.     {
  5.         Console.WriteLine("Checking site: '{0}' ({1})", site.RootWeb.Title, site.Url);
  6.  
  7.         Type spSiteType = typeof(SPSite);
  8.  
  9.         // site must be initialized before accessing the contact info
  10.         MethodInfo mi_InitSite = spSiteType.GetMethod("InitSite",
  11.                         BindingFlags.NonPublic | BindingFlags.Instance, null,
  12.                         new Type[0], null);
  13.  
  14.         if (mi_InitSite != null)
  15.         {
  16.             mi_InitSite.Invoke(site, null);
  17.  
  18.             // get field info of m_OwnerID
  19.             FieldInfo fi_m_OwnerID = spSiteType.GetField("m_OwnerID",
  20.                     BindingFlags.NonPublic | BindingFlags.Instance);
  21.             // get field info of m_nSecondaryContactID
  22.             FieldInfo fi_m_nSecondaryContactID = spSiteType.GetField("m_nSecondaryContactID",
  23.                     BindingFlags.NonPublic | BindingFlags.Instance);
  24.  
  25.             String currentAdmin = String.Empty;
  26.             bool isFound = false;
  27.  
  28.             if (fi_m_OwnerID != null)
  29.             {
  30.                 int primaryContactId = (int)fi_m_OwnerID.GetValue(site);
  31.                 isFound = GetUserName(site, primaryContactId, out currentAdmin);
  32.                 Console.WriteLine("Primary: {0}; {1}", primaryContactId, currentAdmin);
  33.                 if (!isFound)
  34.                 {
  35.                     Console.WriteLine("Fix site owner to '{0}'", adminUser);
  36.                     site.Owner = site.RootWeb.EnsureUser(adminUser);
  37.                 }
  38.             }
  39.  
  40.             if (fi_m_nSecondaryContactID != null)
  41.             {
  42.                 int secondaryContactId = (int)fi_m_nSecondaryContactID.GetValue(site);
  43.                 isFound = GetUserName(site, secondaryContactId, out currentAdmin);
  44.                 Console.WriteLine("Secondary: {0}; {1}", secondaryContactId, currentAdmin);
  45.                 if (!isFound)
  46.                 {
  47.                     Console.WriteLine("Fix secondary contact to '{0}'", adminUser);
  48.                     site.SecondaryContact = site.RootWeb.EnsureUser(adminUser);
  49.                 }
  50.             }
  51.         }
  52.     }
  53.     catch (Exception ex)
  54.     {
  55.         Console.WriteLine(ex.Message);
  56.     }
  57. }

If the user set as the admin is invalid, we simply replace it with the default value passed in the adminUser parameter.

Side note: In the former post I wrote a few words about the internal GetByIDNoThrow method and the private FindUserNoThrow method of the SPUserCollection class. These method may help to create alternative ways to lookup the user, but it is important to let the caller method know, that we found no user because none was set or because the one that was set cannot be resolved. So passing back an SPUser instance with a simple null value is not a solution.

Our implementation of GetUserName method looks like illustrated by the following code block:

  1. private bool GetUserName(SPSite site, int userId, out String currentAdmin)
  2. {
  3.     currentAdmin = "Not specified";
  4.     bool isFound = true;
  5.  
  6.     if (userId != 0)
  7.     {
  8.         SPUser user = site.RootWeb.SiteUsers.Cast<SPUser>().AsQueryable().FirstOrDefault(usr => usr.ID == userId);
  9.         currentAdmin = (user == null) ? "Unknown user" : user.Name;
  10.         isFound = (user != null);
  11.     }
  12.  
  13.     return isFound;
  14. }

Running the code as part of a console application via calling the CheckSiteAdminsOnFarm method displays the current site owners and secondary contacts of all the sites of all the web applications, and replaces the invalid values if necessary.

Displaying the list of custom actions and custom action groups

As you surely knows, user actions provide an excellent way to extend SharePoint UI. They were there in the WSS 3.0 version as well, although the support was limited to the declarative feature elements. In SharePoint 2010 there is an option to add / remove user actions using SharePoint Designer or custom code as well.

Recently I was to query for some custom actions in SharePoint 2010. You can find code for that on the web, but it is easy to create our own version as well.

On the server side the SPSite, SPWeb and SPList classes has their UserCustomActions properties (type of SPUserCustomActionCollection that is basically a list of SPUserCustomAction instances), and on the client side the case is similar, having Site, Web and List classes and their UserCustomActions properties (type of UserCustomActionCollection, a list of UserCustomAction instances).

Side note 1 (for advanced readers): These custom actions are stored in the CustomActions table of the content database, and queried through the proc_GetCustomActionsFromScope stored procedure. This SP is called from the LoadUserCustomActionsFromDataSource method of SPUserCustomActionCollection class that is invoked from the Ensure method. The Ensure method is called from the constructors of SPUserCustomActionCollection class that is invoked in the getter of the UserCustomActions property of the appropriate SPSite, SPWeb or SPList object.

Side note 2 (less technical): Seeing the User prefix in the above mentioned property and class names (like UserCustomActions, SPUserCustomActionCollection) raised doubt about whether really these objects can give the solution for my requirement. I had to list built-in custom actions, not user defined ones.

Back to our code, it probably should look like this one on the server side to list custom actions for site / web / list:

  1. SPList list = web.Lists["CustomList"];
  2. web.Site.UserCustomActions.ToList().ForEach(
  3.     customAction => Console.WriteLine("Site custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  4. web.UserCustomActions.ToList().ForEach(
  5.     customAction => Console.WriteLine("Web custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  6. list.UserCustomActions.ToList().ForEach(
  7.     customAction => Console.WriteLine("List custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));

And on the client side the code is very similar:

  1. ClientContext clientContext = new ClientContext("http://sp2010&quot;);
  2. Site site = clientContext.Site;
  3. Web web = clientContext.Web;
  4. List list = web.Lists.GetByTitle("CustomList");
  5. clientContext.Load(site, s => s.UserCustomActions);
  6. clientContext.Load(web, w => w.UserCustomActions);
  7. clientContext.Load(list, l => l.UserCustomActions);
  8. clientContext.ExecuteQuery();
  9. site.UserCustomActions.ToList().ForEach(
  10.     customAction => Console.WriteLine("Site custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  11. web.UserCustomActions.ToList().ForEach(
  12.     customAction => Console.WriteLine("Web custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));
  13. list.UserCustomActions.ToList().ForEach(
  14.     customAction => Console.WriteLine("List custom action title: '{0}', description: '{1}'", customAction.Title, customAction.Description));

I’ve checked both codes, but as I expected, they did not provide the solution I needed, only the few custom actions created earlier by me were listed.

So where to go next? I had to dig deeper…

It was not hard to find the internal SPCustomActionElement class (Microsoft.SharePoint namespace in the Microsoft.SharePoint assembly), and on that track I was able to get to the internal QueryForCustomActions method of the  internal SPElementProvider class (same namespace and assembly as above). There are two overloads for  the QueryForCustomActions  method, the first has this parameter pattern:

SPWeb web, SPList list, string scope, string location, string groupId

The second one has an extra bool parameter called ignoreRights. This version used internally by the first one passing the last parameter as false. Now I did not want to trick with permissions, so I chose the simpler first version.

Side note 3: There are a lot of useful methods in SPElementProvider, so I suggest you to have a closer look at this class. In this post I will use only QueryForCustomActions  and QueryForCustomActionGroups methods.

The following code demonstrates how to call QueryForCustomActions  using reflection to display non-user custom action information. The sample provided is far to be called performance optimized, but it is a good starting point to understand the method of working with internal classes and methods.

The DisplayCustomActions method calls the QueryForCustomActions  method and iterates through the result, displaying info through the DisplayCustomAction method. This method receives an Object that should be type of the internal SPCustomActionElement (we can’t declare its type at design time, since it is internal), and displays its string-based properties specified in the propsToDisplay array.

  1. private void DisplayCustomActions(SPWeb web, SPList list, String scope, String location, String groupId)
  2. {
  3.     // hack to get the Microsoft.SharPoint assembly
  4.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  5.     // and a reference to the type of the SPElementProvider internal class
  6.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  7.  
  8.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  9.          null, new Type[0], null);
  10.  
  11.     if (ci_SPElementProvider != null)
  12.     {
  13.         // spElementProvider will be of type internal class
  14.         // Microsoft.SharePoint.SPElementProvider
  15.         // defined in Microsoft.SharePoint assembly
  16.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  17.  
  18.         if (spElementProvider != null)
  19.         {
  20.             // we call
  21.             // internal List<SPCustomActionElement> QueryForCustomActions(SPWeb web, SPList list, string scope, string location, string groupId)
  22.  
  23.             MethodInfo mi_QueryForCustomActions = spElementProviderType.GetMethod("QueryForCustomActions",
  24.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  25.                     new Type[] { typeof(SPWeb), typeof(SPList), typeof(String), typeof(String), typeof(String) }, null
  26.                     );
  27.             if (mi_QueryForCustomActions != null)
  28.             {
  29.                 // result is List<SPCustomActionElement>
  30.                 IEnumerable customActions = (IEnumerable)mi_QueryForCustomActions.Invoke(spElementProvider,
  31.                     new Object[] { web, list, scope, location, groupId });
  32.                 customActions.Cast<Object>().AsQueryable().ToList().ForEach(
  33.                     customAction => DisplayCustomAction(customAction,
  34.                     "Title", "Description", "GroupId", "Location"));
  35.             }
  36.         }
  37.     }
  38. }
  39.  
  40. private void DisplayCustomAction(object customAction, params String[] propsToDisplay)
  41. {
  42.     // hack to get the Microsoft.SharPoint assembly
  43.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  44.     // and a reference to the type of the SPCustomActionElement internal class
  45.     Type spCustomActionElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionElement");
  46.  
  47.     // runtime check the type of the parameter
  48.     if (customAction.GetType() == spCustomActionElementType)
  49.     {
  50.         List<String> propValues = new List<String>();
  51.         propsToDisplay.ToList().ForEach(propToDisplay =>
  52.             {
  53.                 System.Reflection.PropertyInfo pi = spCustomActionElementType.GetProperty(
  54.                     propToDisplay, BindingFlags.Public | BindingFlags.Instance);
  55.                 if (pi != null)
  56.                 {
  57.                     propValues.Add(String.Format("{0}: {1}", propToDisplay, pi.GetValue(customAction, null)));
  58.                 }
  59.             }
  60.         );
  61.         if (propValues.Count > 0)
  62.         {
  63.             Console.WriteLine(String.Format(String.Join("; ", propValues.ToArray())));
  64.         }
  65.         
  66.     }
  67. }

The second code example is for the user action groups, it is very similar to the first code block. In this case we display Title, RequiredAdmin (the admin level required for this element) and Id (this one is not defined on the SPCustomActionGroupElement level, but inherited from the Microsoft.SharePoint.Administration.SPElementDefinition base class).

  1. private void DisplayCustomActionGroups(SPWeb web, String scope, String location)
  2. {
  3.     // hack to get the Microsoft.SharPoint assembly
  4.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  5.     // and a reference to the type of the SPElementProvider internal class
  6.     Type spElementProviderType = sharePointAssembly.GetType("Microsoft.SharePoint.SPElementProvider");
  7.  
  8.     ConstructorInfo ci_SPElementProvider = spElementProviderType.GetConstructor(BindingFlags.Public | BindingFlags.Instance,
  9.          null, new Type[0], null);
  10.  
  11.     if (ci_SPElementProvider != null)
  12.     {
  13.         // spElementProvider will be of type internal class
  14.         // Microsoft.SharePoint.SPElementProvider
  15.         // defined in Microsoft.SharePoint assembly
  16.         Object spElementProvider = ci_SPElementProvider.Invoke(null);
  17.  
  18.         if (spElementProvider != null)
  19.         {
  20.             // we call
  21.             // internal List<SPCustomActionGroupElement> QueryForCustomActionGroups(SPWeb web, SPList list, string scope, string location, string groupId)
  22.  
  23.             MethodInfo mi_QueryForCustomActionGroups = spElementProviderType.GetMethod("QueryForCustomActionGroups",
  24.                     BindingFlags.NonPublic | BindingFlags.Instance, null,
  25.                     new Type[] { typeof(SPWeb), typeof(String), typeof(String) }, null
  26.                     );
  27.             if (mi_QueryForCustomActionGroups != null)
  28.             {
  29.                 // result is List<SPCustomActionGroupElement>
  30.                 IEnumerable customActionGroups = (IEnumerable)mi_QueryForCustomActionGroups.Invoke(spElementProvider,
  31.                     new Object[] { web, scope, location });
  32.                 customActionGroups.Cast<Object>().AsQueryable().ToList().ForEach(
  33.                     customActionGroup => DisplayCustomActionGroup(customActionGroup,
  34.                     "Title", "Id", "RequiredAdmin"));
  35.             }
  36.         }
  37.     }
  38. }
  39.  
  40. private void DisplayCustomActionGroup(object customActionGroup, params String[] propsToDisplay)
  41. {
  42.     // hack to get the Microsoft.SharPoint assembly
  43.     Assembly sharePointAssembly = typeof(SPWeb).Assembly;
  44.     // and a reference to the type of the SPCustomActionGroupElement internal class
  45.     Type spCustomActionGroupElementType = sharePointAssembly.GetType("Microsoft.SharePoint.SPCustomActionGroupElement");
  46.  
  47.     // runtime check the type of the parameter
  48.     if (customActionGroup.GetType() == spCustomActionGroupElementType)
  49.     {
  50.         List<String> propValues = new List<String>();
  51.         propsToDisplay.ToList().ForEach(propToDisplay =>
  52.         {
  53.             System.Reflection.PropertyInfo pi = spCustomActionGroupElementType.GetProperty(
  54.                 propToDisplay, BindingFlags.Public | BindingFlags.Instance);
  55.             if (pi != null)
  56.             {
  57.                 propValues.Add(String.Format("{0}: {1}", propToDisplay, pi.GetValue(customActionGroup, null)));
  58.             }
  59.         }
  60.         );
  61.         if (propValues.Count > 0)
  62.         {
  63.             Console.WriteLine(String.Format(String.Join("; ", propValues.ToArray())));
  64.         }
  65.  
  66.     }
  67. }

And here is a short example about the usage of the above methods:

  1. DisplayCustomActionGroups(web, null, "Microsoft.SharePoint.SiteSettings");
  2. DisplayCustomActions(web, null, null, "Microsoft.SharePoint.SiteSettings", "SiteAdministration");
  3. DisplayCustomActions(web, list, "Site", "CommandUI.Ribbon", null);

Unfortunately, the above described approach is not usable from client side code.

Note, that although you can specify scope, location, and group information, you can pass null for example for scope and location. In this case the custom actions are not filtered for that value.

For a list of possible values for location and group IDs, see the page on MSDN:

Default Custom Action Locations and IDs

Regarding the scope parameter, I found, that when you set something totally wrong, like “MyScope”, then the following exception is thrown (you should check it in the InnerException property of the top level exception System.Reflection.TargetInvocationException):

ArgumentException "Invalid feature scope ‘MyScope’. Valid values are Farm, WebApplication, Site, or Web."

The exception is thrown, because validation in the StringToScope method in SPFeature class failed. Valid values are there:
"Farm", "WebApplication", "WssWebApplication", "Site", "Web"

However, unless you set “Site” or “Web” as scope, you get another ArgumentException exception with a simple Message property saying "scope". It comes from StringToUserCustomActionScope method in SPUserCustomAction class, where valid values are only:
"Site", "Web", "List"

So it means we are limited here to “Site” or “Web” scoped custom actions. But what is then that SPList parameter when calling QueryForCustomActions? As far as I see from the reflected code, it is used only to aggregate user custom action into the result and a permission check. How to get then list scoped custom actions, ECBs, etc.? Well, I think it should be another post…

May 8, 2011

Hunting for the lost Advanced search link

Filed under: Dynamic method, Reflection, Search, SP 2010, Web part — Tags: , , , , — Peter Holpar @ 01:37

The other day I had to configure SharePoint search web parts. The plans were to create a Search Center, and set each web parts to include the Advanced search link. After configuring the People Search Box web part I realized that nothing seems to be changed on the UI.

image

I’ve double-checked the settings and found that the Display advanced search link checkbox was checked and the URL of the advanced search page was specified in the Advanced Search page URL text box, so the advanced link should be there.

image

I found a question on the web about the same issue (based on its date, probably for MOSS 2007) but no answer. So I started Reflector to see what’s happening in the web part.

Here are the results:

PeopleSearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.SharePoint.Portal assembly) is responsible for rendering the People Search Box web part. This class is inherited from the SearchBoxEx class (Microsoft.SharePoint.Portal.WebControls namespace, Microsoft.Office.Server.Search assembly) that is behind the standard Search Box web part, where the Advanced link was displayed with the very same settings.

image

There is an internal virtual CreateAdvanceSearchLink method in the SearchBoxEx class that is overridden in the PeopleSearchBoxEx class. This method is called from the CreateChildControls method of the corresponding class. I suspected that the issue is somewhere there, and yes, although the SearchBoxEx version of the CreateAdvanceSearchLink method handles the advanced search link settings, the PeopleSearchBoxEx version does not, nor does it call its base class version.

So next question, how to fix this problem? In the following I show you a solution for that, although I have to admit, it is neither trivial nor probably supported, as it uses techniques that may fail after a SharePoint service pack. I suggest you to use it at your own risk, if you decide to use it at all. My real goal of publishing this workaround is to show you some “dirty” programming methods that may help you to give solutions in such situations.

If you are a regular reader of my posts, you may already know that I like using reflection to call hidden methods to enable features that is not enabled through the public SharePoint API. In this case this was my first intention either.

The CreateAdvanceSearchLink method has three parameters. The first one is a TableRow, it is the row of the search box, and the method adds a new cell to the row if there is a link to display. The second parameter is a String, it is the search keyword that is typically got from the k query string parameter and appended to the advanced search link URL. Although this parameter exists for the PeopleSearchBoxEx class version as well, it is not used in that implementation. The last parameter is an integer value that is passed by reference. It contains the actual cell count of the row, and is incremented if a new cell is added to the row in the method. It might be not the most elegant way of handling that but it works this way.

So I created a custom web part derived from the PeopleSearchBoxEx class and was to call the original CreateAdvanceSearchLink version (in the base-base class SearchBoxEx) using reflection that would create the lost link for me if the web part settings require that.

This code gets the  MethodInfo of the the  internal CreateAdvanceSearchLink method of the SearchBoxEx type, casts the current web part instance to the SearchBoxEx class and invoke the MethodInfo on that instance. It typically looks like that:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     Type searchBoxExType = typeof(SearchBoxEx);
  4.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  5.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  6.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  7.     if (mi_CreateAdvanceSearchLink != null)
  8.     {
  9.         object[] args = { row, keyword, colsRest };
  10.         SearchBoxEx searchBoxEx = (SearchBoxEx)this;
  11.         mi_CreateAdvanceSearchLink.Invoke(searchBoxEx, args);
  12.         colsRest = (int)args[2];
  13.     }
  14. }

After the first test I found, that in this case not the SearchBoxEx version of the CreateAdvanceSearchLink method is called, but the PeopleSearchBoxEx version. That is because the original version is marked as virtual, so even reflection calls the overridden version.

What can we do in this case to force the .NET framework to call the original version? One can find the answer for this question in this forum thread. Although the answer from Doggett is not the accepted answer for the question at the time of writing this post, I found it useful to find the right way.

So let’s try the same using a dynamic method!

After declaring the delegate that corresponds to the signature of the CreateAdvanceSearchLink method (including the type itself), I created a member variable to store the reference to the delegate that I create in the class constructor. That is reasonable, as it does not change between calls (if we were to call it multiple times) so it helps better performance.

In this case we get the MethodInfo similarly to our first try, then use the ILGenerator class to generate the DynamicMethod. Finding the correct IL code requires some knowledge of the CLR and usually done through the trial and error approach (at least, for me).

Finally, we create the delegate instance using our DynamicMethod.

  1. delegate void CreateAdvanceSearchLinkDelegate(PeopleSearchBoxEx peopleSearchBoxEx, TableRow row, String keyword, ref int colsRest);
  2.  
  3. CreateAdvanceSearchLinkDelegate _createAdvanceSearchLink;
  4.  
  5. public PeopleSearchBoxExAdv() : base()
  6. {
  7.     Type searchBoxExType = typeof(SearchBoxEx);
  8.     Type[] parameterTypes = { typeof(TableRow), typeof(String), typeof(int).MakeByRefType() };
  9.     MethodInfo mi_CreateAdvanceSearchLink = searchBoxExType.GetMethod("CreateAdvanceSearchLink",
  10.             BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly, null, parameterTypes, null);
  11.     if (mi_CreateAdvanceSearchLink != null)
  12.     {
  13.         DynamicMethod dm = new DynamicMethod("CreateAdvanceSearchLink", null,
  14.             new Type[] { typeof(PeopleSearchBoxEx), typeof(TableRow), typeof(String), typeof(int).MakeByRefType() },
  15.             typeof(PeopleSearchBoxEx));
  16.         ILGenerator gen = dm.GetILGenerator();
  17.         gen.Emit(OpCodes.Ldarg_0);
  18.         gen.Emit(OpCodes.Ldarg_1);
  19.         gen.Emit(OpCodes.Ldarg_2);
  20.         gen.Emit(OpCodes.Ldarg_3);
  21.         gen.Emit(OpCodes.Call, mi_CreateAdvanceSearchLink);
  22.         gen.Emit(OpCodes.Ret);
  23.         _createAdvanceSearchLink =
  24.             (CreateAdvanceSearchLinkDelegate)dm.CreateDelegate(typeof(CreateAdvanceSearchLinkDelegate));
  25.     }

Having this done, calling the original version of the CreateAdvanceSearchLink method is so simple as this:

  1. protected void CreateAdvanceSearchLink(TableRow row, string keyword, ref int colsRest)
  2. {
  3.     if (_createAdvanceSearchLink != null)
  4.     {
  5.         _createAdvanceSearchLink((PeopleSearchBoxEx)this, row, keyword, ref colsRest);
  6.     }
  7. }

The rest of the code is about implementing the addition or merging the link as requested. If there are other links to display (like Preferences or Search Options) the we have to merge the Advanced link with these ones, otherwise, we have to add the new cell as is.

We have to handle the search keyword that we need to pass to the CreateAdvanceSearchLink method. The keyword itself is accessible through the protected field m_strKSFromPostOrGetOverride of the base class. In the SearchBoxEx implementation of the method it is trimmed using the internal static TrimAndChopStringBySize method of the internal SearchCommon class. Since it is a quite simple method and I did not want to involve more reflection into the code, I simple borrowed the method into my class:

  1. string TrimAndChopStringBySize(string strLongString, int iMaxSize)
  2. {
  3.     if (strLongString == null)
  4.     {
  5.         return string.Empty;
  6.     }
  7.     string str = strLongString.Trim();
  8.     int length = str.Length;
  9.     if (iMaxSize >= length)
  10.     {
  11.         return str;
  12.     }
  13.     if (iMaxSize > 3)
  14.     {
  15.         return (str.Substring(0, iMaxSize – 3) + "…");
  16.     }
  17.     return str.Substring(0, iMaxSize);
  18. }

If we add the Advanced link to a new cell of the row, and there is a second row in the web part for the Additional query description label, then we have to ensure that the column span of its cell is increased by one.

Based on the above, the CreateChildControls method of our class is as follows:

  1. protected override void CreateChildControls()
  2. {
  3.     base.CreateChildControls();
  4.  
  5.     // we have to alter the content only if advanced search link
  6.     // must be shown
  7.     if (ShowAdvancedSearch)
  8.     {
  9.         // table might be at first or later position
  10.         // depending on web part settings
  11.         Table table = null;
  12.         foreach (Control control in Controls)
  13.         {
  14.             if (control is Table)
  15.             {
  16.                 table = (Table)control;
  17.                 break;
  18.             }
  19.         }
  20.  
  21.         // table found
  22.         if (table != null)
  23.         {
  24.             //string str5 = SearchCommon.TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((this._AppQueryTerms != null) ? this._AppQueryTerms.Length : 0));
  25.             String keyword = TrimAndChopStringBySize(this.m_strKSFromPostOrGetOverride, 200 – ((AppQueryTerms != null) ? AppQueryTerms.Length : 0));
  26.             //String keyword = Page.Request.QueryString["k"];
  27.  
  28.             // should be always true, but check to be sure…
  29.             if (table.Rows.Count > 0)
  30.             {
  31.                 int colsRest;
  32.                 // if either preferences or options are shown
  33.                 // we have to merge the advanced search link
  34.                 // into the existing cell
  35.                 if ((ShowPerferenceLink) || (ShowSearchOptions))
  36.                 {
  37.                     TableRow tr = new TableRow();
  38.                     colsRest = 0;
  39.  
  40.                     CreateAdvanceSearchLink(tr, keyword, ref colsRest);
  41.  
  42.                     int itemNum = (ShowPerferenceLink) ? 1 : 0;
  43.  
  44.                     // should be always true, but check to be sure…
  45.                     if ((tr.Cells.Count > 0) && (tr.Cells[0].Controls.Count > itemNum) &&
  46.                         (tr.Cells[0].Controls[itemNum] is HtmlGenericControl) &&
  47.                         (table.Rows[0].Cells.Count > 1))
  48.                     {
  49.                         // copy the 'entire DIV' tag, not the HyperLink only
  50.                         // into the next to last cell position
  51.                         HtmlGenericControl gc = (HtmlGenericControl)tr.Cells[0].Controls[itemNum];
  52.                         table.Rows[0].Cells[table.Rows[0].Cells.Count – 2].Controls.Add(gc);
  53.                     }
  54.  
  55.                 }
  56.                 // if neither preferences nor options are shown
  57.                 // we can add the new cell to the end of
  58.                 // of the existing row
  59.                 else
  60.                 {
  61.                     colsRest = table.Rows[0].Cells.Count; ;
  62.                     CreateAdvanceSearchLink(table.Rows[0], keyword, ref colsRest);
  63.  
  64.                     if (AppQueryTermsLabel != null)
  65.                     {
  66.                         // there must be a second line if the Additional query description label
  67.                         // is specified, but we check to be sure…
  68.                         if (table.Rows.Count > 1)
  69.                         {
  70.                             table.Rows[1].Cells[table.Rows[1].Cells.Count – 1].ColumnSpan++;
  71.                         }
  72.                     }
  73.                 }
  74.  
  75.             }
  76.  
  77.         }
  78.     }
  79. }

To test the code, we add the web part to the search center:

image

After setting the web part properties, including the ones that are responsible for the with of the web part and the search box, the result is as it should be out of the box:

image

You can download the sample from here.

March 3, 2011

A more developer-friendly way of programmatically creating SharePoint 2010 external content types

Filed under: BCS, Reflection, SP 2010 — Tags: , , — Peter Holpar @ 00:51

Todd Baginski held a presentation (SPC 405 – Business Connectivity Services Runtime and Object Model Deep Dive) on 2009 Microsoft SharePoint Conference about creating ECTs from code. You can read more about it and find the original code on his blog.

The code in Todd’s post is really valuable when learning the object model, but I found that creating ECTs this way is very error-prone. If you need to alter the model, you have serious chance to make something wrong.

In this post I show you the main issues with that approach and try to offer you an alternative way to achieve the same result in a more developer-friendly manner.

First, I don’t like specifying type descriptor parameters as string values. It is longer than necessary and easy to mistype.

  1. TypeDescriptor returnRootCollectionTypeDescriptor2 =
  2.     customersParameter.CreateRootTypeDescriptor("Customers", true,
  3.     "System.Data.IDataReader, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
  4.     "Customers", null, null, TypeDescriptorFlags.IsCollection, null, catalog);

As a first step, one could compute the string from the original type and store it in string variables as shown here:

  1. String int32TypeName = typeof(Int32).ToString(); // will be "System.Int32"
  2. String stringTypeName = typeof(String).ToString(); // will be "System.String"
  3. String iDataReaderTypeName = typeof(IDataReader).AssemblyQualifiedName; // will be "System.Data.IDataReader, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
  4. String iDataRecordTypeName = typeof(IDataRecord).AssemblyQualifiedName; // will be "System.Data.IDataRecord, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"

Later, these string values could be used as parameters of type descriptor creation.

The ideal solution would be to use the Type itself as the parameter of these methods. We will see how to achieve that soon.

Second, the parameters of the methods are rather redundant (see the name and lobName parameters that are usually the same value), contain several unspecified (null) parameter values, and the calls to create different type descriptors share most of their parameter values.

  1. returnRootElementTypeDescriptor.ChildTypeDescriptors.Create("FirstName", true,
  2.     "System.String", "FirstName", null, null, TypeDescriptorFlags.None, null);
  3. returnRootElementTypeDescriptor.ChildTypeDescriptors.Create("LastName", true,
  4.     "System.String", "LastName", null, null, TypeDescriptorFlags.None, null);
  5. returnRootElementTypeDescriptor.ChildTypeDescriptors.Create("Phone", true,
  6.     "System.String", "Phone", null, null, TypeDescriptorFlags.None, null);

It would be nice to create a base parameter set, without the need to specify default (null) values, and specify only the unique values on method calls.

Third, the calls to create type descriptors are sometimes rather complex and contains redundant parameter values again.

  1. customerIDParameter.CreateRootTypeDescriptor("CustomerId", true, "System.Int32", "CustomerId",
  2.     new IdentifierReference("CustomerId",
  3.     new EntityReference("AdventureWorks", "Customer", catalog), catalog),
  4.     null, TypeDescriptorFlags.None, null, catalog);

Wouldn’t it be great to get the entity and identifier references automatically from the entities and identifiers of the model already specified?

I’ve tried to achieve the above goals by applying a few extension methods and creating a custom type to hold type descriptor creation parameters.

The following two extension methods show how to get the IdentifierReference and the EntityReference by specifying the Identifier and the Entity:

  1. public static IdentifierReference GetReference(this Identifier identifier)
  2. {
  3.     // to protect method against possible direct call with null value
  4.     // should not happen when called like a "standard" extension method
  5.     if (identifier == null)
  6.     {
  7.         throw new ArgumentNullException("identifier");
  8.     }
  9.  
  10.     Entity entity = identifier.Entity;
  11.     EntityReference entityReference = entity.GetReference();
  12.     IdentifierReference result = new IdentifierReference(identifier.Name, entityReference, entity.GetCatalog());
  13.  
  14.     return result;
  15. }
  16.  
  17. public static EntityReference GetReference(this Entity entity)
  18. {
  19.     // to protect method against possible direct call with null value
  20.     // should not happen when called like a "standard" extension method
  21.     if (entity == null)
  22.     {
  23.         throw new ArgumentNullException("entity");
  24.     }
  25.  
  26.     // IMPORTANT! name and namespace parameters are in opposite order
  27.     // in the case of Entity.Create method and EntityReference constructor
  28.     EntityReference result = new EntityReference(entity.Namespace, entity.Name, entity.GetCatalog());
  29.  
  30.     return result;
  31. }

Just a side note. As you can read in the comments, name and namespace parameters are in opposite order in the case of Entity.Create method and EntityReference constructor. I find this design to be quite misleading.

Both methods above need a reference to the parent AdministrationMetadataCatalog. Instead of passing this as a parameter, I applied a Reflection call to get its value from the Entity that contains it as a non-public field. This extension method might be nicer as an extension property, but unfortunately there is not yet such thing in .NET.

  1. // unfortunately, there is no (yet) "extension property" in .NET
  2. // so we must create a "get" extension method instead
  3. public static AdministrationMetadataCatalog GetCatalog(this Entity entity)
  4. {
  5.     // to protect method against possible direct call with null value
  6.     // should not happen when called like a "standard" extension method
  7.     if (entity == null)
  8.     {
  9.         throw new ArgumentNullException("entity");
  10.     }
  11.  
  12.     AdministrationMetadataCatalog result = null;
  13.  
  14.     MethodCollection methods = entity.Methods;
  15.  
  16.     // a new MethodColletion is created on each Entity creation (see internal Entity constructor)
  17.     // so it shouldn't be null
  18.     if (methods != null)
  19.     {
  20.         Type methodCollectionType = typeof(MethodCollection);
  21.         FieldInfo fi_metadataCatalog = methodCollectionType.GetField("metadataCatalog", BindingFlags.NonPublic | BindingFlags.Instance);
  22.         result = (AdministrationMetadataCatalog)fi_metadataCatalog.GetValue(methods);
  23.     }
  24.  
  25.     return result;
  26. }

You can use these methods as illustrated here:

  1. Entity customerEntity = Entity.Create("Customer", "AdventureWorks", true,
  2.     new Version("1.0.0.0"), 1000, CacheUsage.Default, lobSystem, customerModel, catalog);
  3. EntityReference customerEntityRef = customerEntity.GetReference();
  4.  
  5. // create the identifier
  6. Identifier customerIdentifier = customerEntity.CreateIdentifier("CustomerID", true, typeof(Int32));
  7. IdentifierReference customerIdentifierRef = customerIdentifier.GetReference();

Next, I’ve created a class to hold type descriptor creation parameters. I’ve planned this class to support inheriting values from another instance of the class through its constructor. Only values explicitly set must be inherited, but default values must not. It requires the class to remember which values were specified explicitly. (Remark: in this version of code this behavior has not too much importance. I planned a merge functionality either that needs this feature in a possible later version.) The class is able to resolve name and lobName parameter values from each other so one should only specify one of them if they are the same.

  1. public class TypeDescriptorParams
  2. {
  3.     private String _name;
  4.     private bool _isNameSpecified = false;
  5.     public String Name { get
  6.     {
  7.         return _name;
  8.     }
  9.         set
  10.         {
  11.             _isNameSpecified = true;
  12.             _name = value;
  13.         }
  14.     }
  15.  
  16.     private bool _isCached;
  17.     private bool _isIsCachedSpecified = false;
  18.     public bool IsCached
  19.     {
  20.         get
  21.         {
  22.             return _isCached;
  23.         }
  24.         set
  25.         {
  26.             _isIsCachedSpecified = true;
  27.             _isCached = value;
  28.         }
  29.     }
  30.  
  31.     private Type _type;
  32.     private bool _isTypeSpecified = false;
  33.     public Type Type
  34.     {
  35.         get
  36.         {
  37.             return _type;
  38.         }
  39.         set
  40.         {
  41.             _isTypeSpecified = true;
  42.             _type = value;
  43.         }
  44.     }
  45.  
  46.     private String _lobName;
  47.     private bool _isLobNameSpecified = false;
  48.     public String LobName
  49.     {
  50.         get
  51.         {
  52.             return _lobName;
  53.         }
  54.         set
  55.         {
  56.             _isLobNameSpecified = true;
  57.             _lobName = value;
  58.         }
  59.     }
  60.  
  61.     private IdentifierReference _identifierReference;
  62.     private bool _isIdentifierReferenceSpecified = false;
  63.     public IdentifierReference IdentifierReference
  64.     {
  65.         get
  66.         {
  67.             return _identifierReference;
  68.         }
  69.         set
  70.         {
  71.             _isIdentifierReferenceSpecified = true;
  72.             _identifierReference = value;
  73.         }
  74.     }
  75.  
  76.     private FilterDescriptor _filterDescriptor;
  77.     private bool _isFilterDescriptorSpecified = false;
  78.     public FilterDescriptor FilterDescriptor
  79.     {
  80.         get
  81.         {
  82.             return _filterDescriptor;
  83.         }
  84.         set
  85.         {
  86.             _isFilterDescriptorSpecified = true;
  87.             _filterDescriptor = value;
  88.         }
  89.     }
  90.  
  91.     private TypeDescriptorFlags _flags;
  92.     private bool _isFlagsSpecified = false;
  93.     public TypeDescriptorFlags Flags
  94.     {
  95.         get
  96.         {
  97.             return _flags;
  98.         }
  99.         set
  100.         {
  101.             _isFlagsSpecified = true;
  102.             _flags = value;
  103.         }
  104.     }        
  105.  
  106.     private AssociationReference _associationReference;
  107.     private bool _isAssociationReferenceSpecified = false;
  108.     public AssociationReference AssociationReference
  109.     {
  110.         get
  111.         {
  112.             return _associationReference;
  113.         }
  114.         set
  115.         {
  116.             _isAssociationReferenceSpecified = true;
  117.             _associationReference = value;
  118.         }
  119.     }
  120.  
  121.     private AdministrationMetadataCatalog _metadataCatalog;
  122.     private bool _isMetadataCatalogSpecified = false;
  123.     public AdministrationMetadataCatalog MetadataCatalog
  124.     {
  125.         get
  126.         {
  127.             return _metadataCatalog;
  128.         }
  129.         set
  130.         {
  131.             _isMetadataCatalogSpecified = true;
  132.             _metadataCatalog = value;
  133.         }
  134.     }
  135.  
  136.     public TypeDescriptorParams()
  137.     {
  138.     }
  139.  
  140.     public TypeDescriptorParams(TypeDescriptorParams explicitParams)
  141.     {
  142.         if (explicitParams._isNameSpecified) this.Name = explicitParams.Name;
  143.         if (explicitParams._isIsCachedSpecified) this.IsCached = explicitParams.IsCached;
  144.         if (explicitParams._isTypeSpecified) this.Type = explicitParams.Type;
  145.         if (explicitParams._isLobNameSpecified) this.LobName = explicitParams.LobName;
  146.         if (explicitParams._isIdentifierReferenceSpecified) this.IdentifierReference = explicitParams.IdentifierReference;
  147.         if (explicitParams._isFilterDescriptorSpecified) this.FilterDescriptor = explicitParams.FilterDescriptor;
  148.         if (explicitParams._isFlagsSpecified) this.Flags = explicitParams.Flags;
  149.         if (explicitParams._isAssociationReferenceSpecified) this.AssociationReference = explicitParams.AssociationReference;
  150.         if (explicitParams._isMetadataCatalogSpecified) this.MetadataCatalog = explicitParams.MetadataCatalog;
  151.     }
  152.  
  153.     public void ResolveNames()
  154.     {
  155.         // names default to the other name type
  156.         if ((this._isNameSpecified) && (!this._isLobNameSpecified))
  157.         {
  158.             this.LobName = this.Name;
  159.         }
  160.  
  161.         if ((!this._isNameSpecified) && (this._isLobNameSpecified))
  162.         {
  163.             this.Name = this.LobName;
  164.         }
  165.     }
  166. }

You can see that we specify the Type and not the String-based type name of the descriptor. It will be resolved later to the name.

Having this class, I created the following extension methods to bridge my code to the standard BCS API calls:

  1. public static TypeDescriptor CreateChildTypeDescriptor(this TypeDescriptor typeDescriptor, TypeDescriptorParams typeDescrParams)
  2. {
  3.     // to protect method against possible direct call with null value
  4.     // should not happen when called like a "standard" extension method
  5.     if (typeDescriptor == null)
  6.     {
  7.         throw new ArgumentNullException("typeDescriptor");
  8.     }
  9.  
  10.     typeDescrParams.ResolveNames();
  11.  
  12.     TypeDescriptor result = typeDescriptor.ChildTypeDescriptors.Create(
  13.         typeDescrParams.Name,
  14.         typeDescrParams.IsCached,
  15.         ResolveForBcs(typeDescrParams.Type),
  16.         typeDescrParams.LobName,
  17.         typeDescrParams.IdentifierReference,
  18.         typeDescrParams.FilterDescriptor,
  19.         typeDescrParams.Flags,
  20.         typeDescrParams.AssociationReference);
  21.  
  22.     return result;
  23. }
  24.  
  25. public static TypeDescriptor CreateRootTypeDescriptor(this Parameter parameter, TypeDescriptorParams typeDescrParams)
  26. {
  27.     // to protect method against possible direct call with null value
  28.     // should not happen when called like a "standard" extension method
  29.     if (parameter == null)
  30.     {
  31.         throw new ArgumentNullException("parameter");
  32.     }
  33.  
  34.     typeDescrParams.ResolveNames();
  35.  
  36.     TypeDescriptor result = parameter.CreateRootTypeDescriptor(
  37.         typeDescrParams.Name,
  38.         typeDescrParams.IsCached,
  39.         ResolveForBcs(typeDescrParams.Type),
  40.         typeDescrParams.LobName,
  41.         typeDescrParams.IdentifierReference,
  42.         typeDescrParams.FilterDescriptor,
  43.         typeDescrParams.Flags,
  44.         typeDescrParams.AssociationReference,
  45.         typeDescrParams.MetadataCatalog);
  46.  
  47.     return result;
  48. }
  49.  
  50. public static Identifier CreateIdentifier(this Entity entity, String name, bool isCached, Type type)
  51. {
  52.     // to protect method against possible direct call with null value
  53.     // should not happen when called like a "standard" extension method
  54.     if (entity == null)
  55.     {
  56.         throw new ArgumentNullException("entity");
  57.     }
  58.  
  59.     Identifier result =
  60.         entity.Identifiers.Create(name, isCached, ResolveForBcs(type));
  61.  
  62.     return result;
  63. }

The following helper method is used by the above methods to resolve the Type to the type name BCS calls require. For CLR library classes we need only the short name of the type, otherwise the qualified name.

  1. private static string ResolveForBcs(Type type)
  2. {
  3.     String typeName = null;
  4.     
  5.     if (type != null)
  6.     {
  7.         typeName = (type.Module.ScopeName == "CommonLanguageRuntimeLibrary") ? type.ToString() : type.AssemblyQualifiedName;
  8.     }
  9.  
  10.     return typeName;
  11. }

Using these concepts, one can write more compact code, no need to specify null and common values. The calls to create type descriptors are easier to read and maintain.

  1. TypeDescriptorParams standardStringType =
  2.     new TypeDescriptorParams
  3.     {
  4.         IsCached = true,
  5.         Type = typeof(Int32),
  6.         Flags = TypeDescriptorFlags.None,
  7.     };
  8.  
  9. TypeDescriptorParams standardIntType =
  10.     new TypeDescriptorParams(standardStringType)
  11.     {
  12.         Type = typeof(Int32),
  13.     };
  14.  
  15. // create the TypeDescriptor for the CustomerID parameter
  16. CustomerIDParameter.CreateRootTypeDescriptor(new TypeDescriptorParams(standardIntType)
  17. {
  18.     Name = "CustomerID",
  19.     IdentifierReference = customerIdentifierRef,
  20.     MetadataCatalog = catalog
  21. });

Repetitive code blocks requires only specifying unique parameter values:

  1. returnElementTypeDescriptor.CreateChildTypeDescriptor(new TypeDescriptorParams(standardStringType)
  2. {
  3.     Name = "FirstName"
  4. });
  5.  
  6. returnElementTypeDescriptor.CreateChildTypeDescriptor(new TypeDescriptorParams(standardStringType)
  7. {
  8.     Name = "LastName"
  9. });
  10.  
  11. returnElementTypeDescriptor.CreateChildTypeDescriptor(new TypeDescriptorParams(standardStringType)
  12. {
  13.     Name = "Phone"
  14. });

This methods help you to write and alter the code easier, but there is still chance to have errors. To find these errors before activating your model, you can use the Validate method of your Entity, and check the returning ActivationError[] array if necessary.

  1. ActivationError[] errors = customerEntity.Validate();
  2.  
  3. result = (errors.Length == 0);
  4.  
  5. if (result)
  6. {
  7.     customerEntity.Activate();
  8.     Console.WriteLine("Model created");
  9. }
  10. else
  11. {
  12.     Console.WriteLine("Validation errors:");
  13.     Array.ForEach(errors,
  14.         error => Console.WriteLine(error));
  15. }

Of course, these examples only provide you some idea about what can be done to create a more maintainable code. There are a lot of things to do yet, for example, BCS connection properties are still string based, that I think not really safe as well:

  1. // set the connection properties
  2. lobSystemInstance.Properties.Add("AuthenticationMode", "PassThrough");
  3. lobSystemInstance.Properties.Add("DatabaseAccessProvider", "SqlServer");
  4. lobSystemInstance.Properties.Add("RdbConnection Data Source", "sp2010");
  5. lobSystemInstance.Properties.Add("RdbConnection Initial Catalog", "AdventureWorksLT");
  6. lobSystemInstance.Properties.Add("RdbConnection Integrated Security", "SSPI");
  7. lobSystemInstance.Properties.Add("RdbConnection Pooling", "true");

You can find the complete solution here, including code from my former posts about checking existence of a list from managed client object model and creating external list from code client side code.

December 23, 2010

How to create WssId for terms that are not yet referenced from the site

Filed under: Reflection, SP 2010, Taxonomies — Tags: , , — Peter Holpar @ 00:45

About a year ago I wrote about TaxonomyFieldValue and its WssId property. In a recent comment I got the question how to get the WssId for terms that are not referenced by the list items on the site.

I tried to answer the question there, but to provide a full example, I decided to post a code sample as well.

As described in the original post, the lookup item for the term and the corresponding WssId is created on the site through the private AddTaxonomyGuidToWss method.

One could call that method directly using reflection to generate the WssId for a term not yet in use on the site.

The following method – having the same name and signature as the original one – illustrates this call:

  1. private int AddTaxonomyGuidToWss(SPSite site, Term term, bool isKeywordField)
  2. {
  3.     int result = -1;
  4.  
  5.     Type taxonomyFieldType = typeof(TaxonomyField);
  6.  
  7.     MethodInfo mi_AddTaxonomyGuidToWss = taxonomyFieldType.GetMethod("AddTaxonomyGuidToWss",
  8.             BindingFlags.NonPublic | BindingFlags.Static, null,
  9.             new Type[3] { typeof(SPSite), typeof(Term), typeof(bool) },
  10.             null
  11.             );
  12.     if (mi_AddTaxonomyGuidToWss != null)
  13.     {
  14.         result = (int)mi_AddTaxonomyGuidToWss.Invoke(null, new object[3] { site, term, isKeywordField });
  15.     }
  16.  
  17.     return result;
  18. }

The original AddTaxonomyGuidToWss method returns the WssId for the lookup item created for the term on the site or –1 if the creation is failed for some reason. Our wrapper method also returns –1 as the default value.

Let’s see a sample for its usage:

  1. private void CreateTermWssIdTest(SPSite site)
  2. {
  3.     TaxonomySession session = new TaxonomySession(site);
  4.     TermStore termStore = session.TermStores["Managed Metadata Service"];
  5.     Group group = termStore.Groups["Test group"];
  6.     TermSet termSet = group.TermSets["Colors"];
  7.     Term term = termSet.Terms["Blue"];
  8.  
  9.     Console.WriteLine("Before creating local lookup item for term '{0}'", term.Name);
  10.     DisplayTermWssIdInfo(site, term);
  11.  
  12.     AddTaxonomyGuidToWss(site, term, false);
  13.  
  14.     Console.WriteLine("After creating local lookup item for term '{0}'", term.Name);
  15.     DisplayTermWssIdInfo(site, term);
  16. }
  17.  
  18. private void DisplayTermWssIdInfo(SPSite site, Term term)
  19. {
  20.     int[] wssIds = TaxonomyField.GetWssIdsOfTerm(site, term.TermStore.Id, term.TermSet.Id, term.Id, false, 1);
  21.  
  22.     Console.WriteLine("Lookup for term '{0}' using GetWssIdsOfTerm. WssId={1}", term.Name, wssIds.Length > 0 ? wssIds[0].ToString() : "Not found!");
  23.     Console.WriteLine("Lookup for term '{0}' using GetWssIdByTermId. WssId={1}", term.Name, GetWssIdByTermId(site.RootWeb, term.Id));
  24. }

First we check for the WssId of a term called “Blue” (assumed not in use on the site), then create the WssId and then try to display again the value of the WssId. For the definition of the GetWssIdByTermId method see the original post.

On a test run we should see something similar:

Before creating local lookup item for term ‘Blue’
Lookup for term ‘Blue’ using GetWssIdsOfTerm. WssId=Not found!
Lookup for term ‘Blue’ using GetWssIdByTermId. WssId=-1
After creating local lookup item for term ‘Blue’
Lookup for term ‘Blue’ using GetWssIdsOfTerm. WssId=1
Lookup for term ‘Blue’ using GetWssIdByTermId. WssId=1

Before creating the local lookup item, the static GetWssIdsOfTerm method of the TaxonomyField class returns an empty array, that is reported as Not found! by the DisplayTermWssIdInfo method. Our helper method GetWssIdByTermId returns the default –1 value.

As I’ve tested the code on a site just created for this test, this is the first lookup item in the hidden lookup list in the site root web. That is why the term added is reported as having value 1 in the WssId.

I hope this simple example helps to better understand the concept of WssId.

Older Posts »

Theme: Shocking Blue Green. Blog at WordPress.com.

Follow

Get every new post delivered to your Inbox.

Join 42 other followers