Author Archive

SharePoint Field Names have 32 Character Limit – So Do Replicated Profile Property Names

Monday, April 13th, 2009

Note: Cross posted from Life on Planet Groove.

Permalink

I ran into a situation where I created a User Profile Property with the Replicable checkbox set, which had a really long name. There were no errors when saving the profile property, however later on I started receiving errors in the Event Log:

failure trying to synch site [guid] for ContentDB [guid] WebApp [guid].  Exception message was A WSS internal name for profile property 'ProfilePropertyWithReallyLongName' could not be found..

Apparently SharePoint has a limit of 32 characters for Internal Field Names. This will also apply to Replicable User Profile Properties since they will get synced down to the WSS Content DB in the UserInfo list.

SharePoint May Break Hyperlinks in Workflow Task Emails

Friday, April 10th, 2009

Note: Cross posted from Life on Planet Groove.

Permalink

I ran into an issue where hyperlinks in workflow task emails where being automatically changed by SharePoint from absolute hyperlinks to relative ones.

Scenario:

  1. You create a custom Visual Studio workflow and use the CreateTask activity.
  2. You set your SPWorkflowTaskProperties.Description property to some HTML text.
  3. In your Description HTML text, you have an html hyperlink <a> tag whose href=”” attribute contains an absolute hyperlink to a resource in the same SharePoint farm (i.e. a list item).
  4. Example: “Please review <a href=”http://yourserver/site/lists/yourlist/dispform.aspx?ID=1”>contract #12345</a>”.

Goal:

Users will receive the task email, and be able to click on the absolute URL in the email body to navigate to the SharePoint resource.

Problem:

It appears that SharePoint parses this HTML and readjusts the hyperlink and makes it relative.

Example: “Please review <a href=”/lists/yourlist/dispform.aspx?ID=1”>contract #12345</a>”

This effectively breaks the hyperlink in the email client.

The only workaround was to get rid of the <a> tag and write out the hyperlink in full. For example:

“Please review contract #12345 (http://yourserver/site/lists/yourlist/dispform.aspx?ID=1)”.

In this manner, SharePoint did not adjust the hyperlink at all.

BDC Error – Type does not match DotNet type of TypeDescriptor

Thursday, April 9th, 2009

Note: Cross posted from Life on Planet Groove.

Permalink

I ran into a BDC error, “Type does not match DotNet type of TypeDescriptor”, with a particular application definition I wrote. In this case, I had a specific finder method that took one input parameter (the identifier) that was of type System.Decimal. It turns out I didn’t specify a default value in the TypeDescriptor for this input parameter in my XML definition. It looks like when you don’t set a default value, it will automatically set one for you and assign it a type of System.String. I was able to see this when I exported the BDC definition fresh, and noticed that SharePoint automatically added this default value and type for me.

Rule of thumb, always specify a default value for each input parameter.

Workflow Task Locked when AssignedTo Changed to Support Multiple Selections

Friday, April 3rd, 2009

Note: Cross posted from Life on Planet Groove.

Permalink

This is an old one, but some co-workers ran into this again recently on a project.

When you have a SharePoint workflow that assigns tasks (either Designer or Visual Studio workflow), when you change the Assigned To column on the task list to allow Multiple Selections, your workflow will hang and become unresponsive when a user edits the task. You’ll receive a message about the task being locked (“This task is currently locked by a running workflow and cannot be edited”). OnTaskChanged events will no longer fire and your workflow will be stuck and unrecoverable.

There is no hotfix or workaround for this, so you simply cannot have the assigned to column support multiple users or groups. Just use a single group instead.

Following are some other resources discussing this:

This Blog has Moved

Thursday, March 5th, 2009

This blog has now moved. I'll continue to cross post relevant SharePoint articles here, but if you would like to stay current with my latest postings, please follow me at the new location:

URL: http://www.lifeonplanetgroove.com

RSS Feed: http://feeds2.feedburner.com/LifeOnPlanetGrooveSharepoint

Thanks,

Adam

Performance Optimizations for Large Programmatic User Profile Imports

Thursday, March 5th, 2009

Note: Cross posted from Life on Planet Groove.
Permalink

I’m working on a project that imports over 1 million users from an Oracle database used with SharePoint/Forms Authentication into the SharePoint user profile store. This is done as a custom SharePoint timer job that pulls the users from the DB and creates/updates User Profiles through the SharePoint API.

When running a job on a recordset of this size, there are several things to strive for:

  • Limit the time that the process needs to run (jobs can take days and overlap themselves)
  • Reduce memory usage (the OWSTIMER.exe can already consume quite a bit with the regular timer jobs)

Two ways you can achieve this:

  • Avoid UserExists() method
  • Use a DataReader if possible  

Avoid UserExists() method

Most code samples on the web that deal with programmatic creation of User Profiles will show code such as this:

if (profileManager.UserExists(accountName)

 

{

 

    userProfile = profileManager.GetUserProfile(accountName)...

 

}

 

else

 

{

 

    userProfile = profileManager.CreateUserProfile(accountName)...

 

}

On small recordsets, this is fine, but for large recordsets the UserExists method represents a bottleneck that can increase the duration that your process runs. In addition, in the code above, you will unknowingly call this method a second useless time, because the CreateUserProfile() method internally calls UserExists() as well.

There are two ways to avoid this method:

  • Cache profile IDs in a Dictionary/Hashtable type object
  • Use reflection to create user profiles

Cache Profile IDs (and MemberGroup IDs too)

The UserProfileManager object is an IEnumerable that you can iterate over and access all the Profiles in SharePoint. Caching the IDs of these profiles up front enables you to index into a Dictionary object to see if your profile exists, rather than hitting SQL Server with UserExists(). The following code helped to reduce processing time significantly (you take a hit up front, but it’s far less than the delay imposed by UserExists over large recordsets):

Dictionary<string, Guid> cachedProfiles = new Dictionary<string, Guid>();

 

foreach(UserProfile profile in profileManager)

 

{

 

    cachedProfiles.Add(profile.AccountName, profile.ID);

 

}

 

...

 

if(cachedProfiles.ContainsKey(accountName)

 

{

 

    ...

 

}

In addition, caching the Guid of the UserProfile lets you later use the overloaded method of GetUserProfile() that takes a Guid as a parameter, which seems to perform slightly better than the alternative that takes a string for AccountName.

This approach also works very well when importing large numbers of MemberGroups:

foreach (MemberGroup memberGroup in memberGroupManager)

 

{

 

    cachedMemberGroups.Add(memberGroup.DisplayName, memberGroup.Id);

 

}

NOTE: If you are wondering why not simply cache the entire UserProfile in the Dictionary (Dictionary<string, UserProfile>), the memory usage for this will be much higher, which will undo any gains by avoiding UserExists().

Use Reflection to Create User Profiles

The UserProfileManager’s CreateUserProfile() method internally calls the UserExists method, and then calls an internal constructor on the UserProfile object to actually create the profile. By using reflection, you can call this internal constructor yourself and avoid UserExists():

// Get some reflected information about the UserProfile object for later use

 

ConstructorInfo ci = typeof(UserProfile).GetConstructor(

 

BindingFlags.NonPublic | BindingFlags.Instance,

 

null,

 

new Type[] { typeof(UserProfileManager), typeof(string), typeof(string) },

 

null);

 

 

Once you’ve got the reflected information, you can use the following code to create your UserProfile:

if (cachedProfiles.ContainsKey(accountName))

 

{

 

    // Get existing profile...

 

}

 

else

 

{

 

    // Create new profile

 

    UserProfile newProfile = (UserProfile)ci.Invoke(new object[] { profileManager, accountName, displayName });

 

}

NOTE: I’ve tried creating a user profile in this manner that already existed to see what would happen. The existing profile was updated, and I did not get any duplicate records in the SharePoint db. It appears the SQL under the hood already takes care of avoiding duplicates. General cautions about reflection still apply here though (API may change, etc.).

Use a DataReader if Possible

Instead of pulling a huge recordset into a DataTable, DataSet, or into a collection of custom objects, try to process your records one at a time using a data reader if your data source permits. This will keep memory usage down, as the garbage collector will dispose frequently any variables you create within a while(reader.Read()) loop. A DataTable with 1 million records in it will take up tons of memory on top of the large memory consumption that OWSTIMER.exe does already.

using (OracleConnection conn = new OracleConnection(_connectionString))

 

{

 

 

 

    using (OracleCommand cmd = new OracleCommand(_sqlGetAllUsers, conn))

 

    {

 

        conn.Open();

 

 

 

        OracleDataReader rdr = cmd.ExecuteReader();

 

 

 

        if(!rdr.HasRows())

 

        {

 

            return;

 

        }

 

 

 

        while (rdr.Read())

 

        {

 

            UserProfile profile = null;

 

            string accountName = rdr["ACCOUNT_NAME"] as string;

 

            string firstName = rdr["FIRST_NAME"] as string;

 

            string lastName = rdr["LAST_NAME"] as string;

 

 

 

            if (cachedProfiles.ContainsKey(accountName))

 

            {

 

                profile = profileManager.GetUserProfile(cachedProfiles[accountName]);

 

            }

 

            else

 

            {

 

                profile = (UserProfile)ci.Invoke(new object[] { profileManager, accountName, displayName });

 

                cachedProfiles.Add(accountName, profile.ID);

 

            }

 

 

 

            if (!string.IsNullOrEmpty(firstName))

 

            {

 

                profile["FirstName"].Value = firstName;

 

            }

 

            if (!string.IsNullOrEmpty(lastName))

 

            {

 

                profile["LastName"].Value = lastName;

 

            }

 

            // ... 

 

            profile.Commit();

 

 

 

        }

 

 

 

        rdr.Close();

 

        rdr.Dispose();

 

 

 

    }

 

}

Of course other best practices also apply, such as:

  • Getting pages of records, rather than all at once
  • Implementing incremental change queries rather than all records all the time
  • Only getting what you need from the data source
  • Disposing your objects and data connections properly

Event Handler for Renaming List Item Attachments

Thursday, December 18th, 2008

I needed the ability to rename list item attachments automatically as attachments were uploaded. I wanted to prefix the filename with the current date/time so that 1) all files would have a unique filename and 2) the files would be sorted in the Display form for the item.

I created an event handler for the ItemAttachmentAdded event. It turns out that in the ItemAttachment… events, you cannot modify the attachment file in any way, including renaming it. The BeforeUrl and After Url properties are read-only, and there is no SPListItem in the SPItemEventProperties object to use. The only handle you can get to the actual file is to traverse the Attachments folder of the list until you get a reference to the actual SPFile object that represents the attachment. The SPFile object has only ReadOnly properties for Title/Name, so you cannot do anything there.

The only way I could find to perform a rename, was to get the binary Byte[] array of the file, create a new attachment with the proper file name, and then delete the original attachment. My code is below:

/// <summary>
/// Asynchronous event occurs after the attachment is added
/// </summary>
/// <param name="properties"></param>
public override void ItemAttachmentAdded(SPItemEventProperties properties)
{
    base.ItemAttachmentAdded(properties);
 
    // Make sure we have at least one attachment (this should never happen, but just in case)
    if (properties.ListItem.Attachments.Count == 0)
    {
        return;
    }
 
    // Get a reference to the attachment
    SPFile attachment = properties.ListItem.ParentList.RootFolder.SubFolders["Attachments"].SubFolders[properties.ListItemId.ToString()].Files[properties.ListItem.Attachments.Count - 1];
 
    // Get the raw data of the file
    byte[] content = attachment.OpenBinary();
 
    // Get the original attachment name
    string originalFileName = attachment.Name;
    // Get the new file name with the date/time stamp
    string newFileName = GetFileNameWithDate(originalFileName);
 
    // Turn off events so that we don't get an infinite loop when we add back the attachment
    DisableEventFiring();
 
    // Delete the original attachment (we can't simply rename the attachment)
    properties.ListItem.Attachments.DeleteNow(originalFileName);
    
    // Add the attachment back with the new filename
    properties.ListItem.Attachments.AddNow(newFileName, content);
 
    // Turn on events again
    EnableEventFiring();
 
}
 
/// <summary>
/// Prefix a file name with the current date time
/// </summary>
/// <param name="fileName">The original file name</param>
/// <returns>A file name prefixed with the date time (e.g. 2008010101010101_test.docx)</returns>
private string GetFileNameWithDate(string fileName)
{
 
    DateTime now = DateTime.Now;
 
    return 
        now.Year.ToString() + 
        now.Month.ToString().PadLeft(2, '0') + 
        now.Day.ToString().PadLeft(2, '0') + 
        now.Hour.ToString().PadLeft(2, '0') + 
        now.Minute.ToString().PadLeft(2, '0') + 
        now.Second.ToString().PadLeft(2, '0') + 
        now.Millisecond.ToString().PadLeft(2, '0') + 
        "_" + fileName;
 
}
 
    }

This had another benefit, in that it made the attachment file names pretty much unique. Unlike documents in doc libraries, new attachments with the same file name as an existing attachment will not override the original, and the user will see an error message from SharePoint when trying to upload the attachment. This gets around that unfriendly experience.

To register my attachment, I used Gary Lapointe's custom stsadm command, AddEventReceiver.

Switch Views when InfoPath Form is Opened

Tuesday, September 16th, 2008

I designed a form for a client that had several different views, and I needed a way to display a specific view when the form was opened. This was easy, but took me a bit to figure out.

  1. Go to Tools > Form Options.
  2. Click the Open and Save option.
  3. Click the Rules button, and add rules and actions to switch views.

With this method, I was able to set a field on form submittal to indicate which view was last active. When the submitted form was re-opened, I was able to use a conditional rule to switch to that last active view.

Make InfoPath Contact Selector Required in Browser-Enabled Forms

Tuesday, September 16th, 2008

If you've used the InfoPath Contact Selector before, you might have seen that it does not support validation like other InfoPath controls do. There is no option on the Properties dialog for the control to make it a required field. To work around this, I used a combination of rules and conditional formatting to prevent the form from being submitted if the control does not have a value.

The easiest way to do this is to create conditional rules in the Submit Options dialog:

  1. Configure your form and add a Contact Selector control (see this article for instructions).
  2. Go to Tools > Submit Options.
  3. Make sure that Allow Users to Submit this Form is checked.
  4. Click the option to Perform custom action using rules.
  5. Click the Rules… button.
  6. In the Rules dialog, click the Add… button.
  7. In the Rule dialog, click the Set Condition… button.
  8. In the Condition dialog, pull down the first dropdown, choose Select a field or group…, and drill down and choose the DisplayName field from your data connection. Select is blank in the second dropdown.
  9. Click the And> button to add a second condition.
  10. In the last dropdown on the first condition, change it from And to Or.
  11. In the second condition, select the DisplayName field again, and choose is not present for the condition. Your two conditions should look like this:
  12. Click OK. In the Rule dialog, do not add any Actions. Check the box to Stop processing rules when this rule finishes.
  13. Make sure that this rule appears first in the list of Rules.

The only downside to this approach is that there is no visual to the user, since InfoPath Forms Services does not support showing a dialog message. The form will just not do anything. To get an even better user experience, You can hide the Submit option from the Toolbar, and use your own Submit buttons with Conditional Formatting:

  1. Drag a button on the page.
  2. Right-click and choose Conditional Formatting….
  3. Click Add… to add conditions.
  4. In the Condition dialog, add the two conditions in steps 8-11 above, and choose Disable this Control.

 

Now, your submit button will be disabled until the user picks a person.

My SharePoint Sites Links Missing – Fix

Tuesday, August 26th, 2008

One client had a problem with profile and membership synchronization, and the "My SharePoint Sites" links would not appear for anyone. This also affected the "SharePoint Sites" and "Membership" web parts on users' MySites – no SharePoint related links would show up in these parts.

We verified that users were explicitly added into the "Members" group of SharePoint sites, we performed full crawls and ran all the timer jobs. Still no dice.

Eventually, a support ticket with MS revealed that having the Content Database set to Offline prevented that functionality from working.