viernes, 10 de octubre de 2008

Adding custom fields to user profiles using CKS FBA solution

I have used CKS FBA (http://www.codeplex.com/cks) lots of times in order to provide authenticated access to public portals over the internet and, one of the most common extensibiliy scenario was adding custom fields to the user profile. The solution was different for each project and I realized I really needed a generic one to be re-used as many times as I wanted to. Some time ago, when designing the architecture of CSP (http://www.codeplex.com/csp) we thought on that generic solution to be used to integrate MOSS and CKS. The middleware component (MembershipInterface) we developed using a WCF contract was the key of this architecture.

Today I've had a new thought: it would be fantastic to have a simpler generic solution, without the need of developing any line of code. So, first of all, I have started looking for existing extensions developed on that direction and I have found this thread: http://www.codeplex.com/CKS/Thread/View.aspx?ThreadId=35189 where Anthony Summer posted an extension of CKS to allow adding custom fields. It was a great start and, after some modifications, I got it to work. Here you are the key points of the solution:

If you read the original thread you will understand perfectly what Anthony was suggesting. Summarizing, all the fields added to the User Information List marked as required would automatically appear in the user registration form and would automatically be saved. The solution was suitable for what I was looking for. Some of my project's requirements where not fullfilled, though:

  • I didn't want the users to be created automatically.
  • I wanted the custom information visible in the membership requests list.

So I started to think how to extend Anthony's class.

First, you need to look for the User Information List into your site collection. For those who doesn't know how to reach this list (it is hidden), you can see it when you access the All People list. Manage this list and add as many fields as you want, ensuring you mark them as required. As you will see in the code, there are some references to this list:

  • web.Lists["User Information List"];
  • site2.RootWeb.Lists[SPUtility.GetLocalizedString("$Resources:userinfo_schema_listtitle", "core", web.Language)];

Anthony was worried, as you will see in his comments, about hardcoding the name of the list, and so was I. Living in a country such as Spain, with several different languages means we are also very concerned about globalization issues so I added another way of accessing this list.

  • web.SiteUserInfoList; // Simple, isn't it?

After customizing this list, go to the FBA membership request list, to be found on the site settings area, and add the same fields you added in the previous list.

So, what do we need in order to make it work?

First, open the MembershipRequest class and add a NameValueCollection member which will host the custom properties

protected NameValueCollection _CustomProperties;
 
public NameValueCollection CustomProperties
{
 get
 {
 if (_CustomProperties == null)
 _CustomProperties = new NameValueCollection();
 
 return _CustomProperties;
 }
 set
 {
 _CustomProperties = value;
 }
}
 

Then, still in this class, locate the ApproveMembership function, and insert this piece of code to add the custom properties to the created user once it becomes accepted.

if (createStatus == MembershipCreateStatus.Success)
{
 //...
 if (!String.IsNullOrEmpty(request.DefaultGroup))
 {
 string name = string.Format("{0}:{1}", System.Web.Security.Membership.Provider.Name.ToLower(), request.UserName.ToLower());
 web.Groups[request.DefaultGroup].AddUser(name, request.UserEmail, request.FirstName + " " + request.LastName, "Self Registration");
 
 if (request.CustomProperties.Count > 0)
 {
 // Instantiates the User Information List 
 SPList userInformationList = web.SiteUserInfoList;
 // Get the current user 
 SPUser user = web.EnsureUser(name);
 // The actual User Information is within this SPListItem 
 SPListItem userItem = userInformationList.Items.GetItemById(user.ID);
 // Whe custom fields are added
 foreach (string customField in request.CustomProperties)
 {
 userItem[customField] = request.CustomProperties[customField];
 }
 userItem.Update();
 }
 //...

Last thing to do on this file. Locate the CopyToReviewList function and add these lines:

foreach (string customField in request.CustomProperties.AllKeys)
{
 if (reviewItem.Fields.ContainsField(customField))
 {
 // TODO: Only working for texts
 reviewItem[customField] = request.CustomProperties[customField];
 }
}

Now, open the MembershipRequestControl class and locate the OnCreatingUser function to insert this portion of code:

Table cuwTable = GetCreateUserTable();
 
foreach (SPField customSignupField in customSignupFields)
{
 // grab matching customfield
 Control customFieldControl = cuwTable.FindControl(customSignupField.StaticName);
 switch (customSignupField.Type)
 {
 case SPFieldType.Text:
 TextBox textBox = (TextBox)customFieldControl;
 request.CustomProperties.Add(customSignupField.StaticName, textBox.Text); 
 break;
 
 case SPFieldType.Note:
 TextBox noteBox = (TextBox)customFieldControl;
 request.CustomProperties.Add(customSignupField.StaticName, noteBox.Text);
 break;
 
 case SPFieldType.MultiChoice:
 CheckBoxList checkBoxList = (CheckBoxList)customFieldControl;
 string value = string.Empty;
 foreach (ListItem item in checkBoxList.Items)
 {
 if (item.Selected)
 {
 value = value + item.Value + ";";
 }
 }
 request.CustomProperties.Add(customSignupField.StaticName, value); 
 
 break;
 
 case SPFieldType.Choice:
 DropDownList dropDown = (DropDownList)customFieldControl;
 request.CustomProperties.Add(customSignupField.StaticName, dropDown.SelectedValue.ToString());
 break;
 
 case SPFieldType.DateTime:
 DateTimeControl dateTime = (DateTimeControl)customFieldControl;
 request.CustomProperties.Add(customSignupField.StaticName, dateTime.SelectedDate.ToLongDateString());
 break;
 }
}
 
MembershipRequest.CopyToReviewList(request);

You should modify also the OnCreatedUser function, but in my case, I need to have the approval process in place, so I haven't done it. Instead of it, I have changed the GetMembershipRequest method into the MembershipReviewHandler class, adding the following piece of code just before the return clause:

SPList userlist = web.SiteUserInfoList;
 
foreach (SPField userField in userlist.Fields)
{
 if (item.Fields.ContainsField(userField.Title))
 {
 if (userField.Required)
 {
 switch (userField.Type)
 {
 case SPFieldType.Text:
 request.CustomProperties.Add(userField.Title, item[userField.Title].ToString());
 break;
 
 case SPFieldType.Note:
 request.CustomProperties.Add(userField.Title, item[userField.Title].ToString());
 break;
 
 
 case SPFieldType.MultiChoice:
 // TODO: not developed yet
 request.CustomProperties.Add(userField.Title, item[userField.Title].ToString());
 break;
 
 case SPFieldType.Choice:
 request.CustomProperties.Add(userField.Title, item[userField.Title].ToString());
 break;
 
 case SPFieldType.DateTime:
 request.CustomProperties.Add(userField.Title, item[userField.Title].ToString());
 break;
 }
 }
 }
}

5 comentarios:

Peter Mularien dijo...

Hi,

Thanks for the write-up. We're trying to implement your code changes using the current release of FBA. When I try to customize the Site Membership Review list, I keep getting "Your changes conflict with those made concurrently by another user." This occurs when I try to create a new column to store the custom data. Did you run into this, and if so, any suggestions?

Thanks!
Peter

David Martos dijo...

Hi Peter,

actually the list you should customize is "User Information List" and not "Site membership review list". The problem you are facing seems a problem on some event firing method within this list. To avoid this error you should change the source code of the event receiver to use these two methods:

this.DisableEventFiring();
// your code
this.EnableEventFiring();

Hope it helps

Peter Mularien dijo...

Hi David,

Thanks for your reply. Actually, based on the way the code is written, it looks like you're intending that the user customize both the User Information List, and the Site Membership Review List - the way I read it, it looks like you're copying the custom field values to the Site Membership Review list prior to the user actually being approved.

Anyhow, I figured out the issue with this - for some reason, when I was logged into the site through FBA (even as the site collection owner), many things weren't working. I set up another zone and logged in with Windows Authentication, and I was then able to customize the Site Membership Review list without a problem.

We ended up not wanting users to have to go through the approval process, so we had to further tweak your solution in order to work when approval wasn't desired -- unfortunately the current mainline code of CKS:FBA doesn't really make this possible without code changes, due to the way that the presence of the features is coded.

Anyhow, I appreciate your posting the code bits - it got us on the right track to a workable solution. It would be nice to be able to fold this back into the CKS:FBA project somehow!

Peter

David Martos dijo...

Hi Peter,

I think CKS team is working on something similar. My post is related to a thread on Codeplex and I sent the code to somebody there. We are also working on a more generic solution based on columns prefixes (instead of mandatory columns) and with multilingual capabilities, but it won't be ready until 2009Q1.

Thanks for your feedback.

Custom Paper Writing dijo...

Many institutions limit access to their online information. Making this information available will be an asset to all.