Archive for the ‘Community Server’ Category

Extending Community Server 2007 User Profiles and Vista Setup

Saturday, November 3rd, 2007

Note:  The pdf version of this post has been removed since I cleaned up the code formatting below.

This post will go over my notes on how I accomplished extending the CS schema and code base in order to: acquire additional user fields on user registration, enable modification of these fields in the user control panel, display the new fields in the public view of the user’s profile, and display the new fields for the user in forum posts.  This post should be useful for developers that want to extend their CS schema and also contains content relevant for users just sticking with extended attributes (for example extending a theme with SubForms is useful for both scenarios).

I have just started digging into Community Server 2007 as a pet project of mine. One thing that I wanted to do was link Community Server users with a set of tables that I already have in place. It appears that CS allows users and other objects to be extended by using extended attributes that save as name value pairs to the database. This did not meet my needs so I needed to dig in and extend CS without much documentation on how to do this.  Following this post will require that you are a fairly proficient .Net developer. The examples that I give here assumes that I have users that are linked to a store and they also have a defined affiliation type to that store.  The stores themselves will be filtered by their US state. For example a user can be a Manager of Acme Markets (and Acme markets only exists in 30 states).  I’m actually developing a different community than this, but I changed to code to make it easier to understand for a blog post. This is also the reason I do not include any screen shots, but trust me, it works!

The first thing I did was to install the Community Server 2007 SDK and install the CS schema as per their directions. I am doing this development on a Windows Vista Ultimate based Virtual PC, so I had to do a few things to get CS up and running after downloading the SDK.

My Windows Vista Specific Development Setup

  • Created Virtual directory – classic pipeline .net 2.0 and pointed it at the location of my CommunityServerWeb20 project
  • Updated CommunityServerWeb20 (Internal).csproj in notepad
    • I changed the IISUrl from “http://cs2007″ to point to my virtual directory http://localhost/cs
    • In hindsight, I probably could have just created a host entry for http://cs2007 to point to my virtual directory
  • Followed additional IIS 7 setup instructions for Visual Studio from the following post: http://mvolo.com/blogs/serverside/archive/2006/12/28/Fix-problems-with-Visual-Studio-F5-debugging-of-ASP.NET-applications-on-IIS7-Vista.aspx
  • Disabled ping / keep alive in IIS7 – This setting caused debugger to hang because its purpose is to recycle the ASP.net worker process if it does not respond to a ping, which can happen if you are sitting on a breakpoint for too long.
  • Updated web.config to have debug=”true”
  • Configured the connection strings to point to my CS database

Updating the Data Layer

Once I had the code running I looked at the CS tables and I saw that there were two tables that were candidates for me to extend: cs_Users and cs_UserProfile. As I got further into the code base, I could not see any compelling reason to choose one table over the other since the data access layer seems to always load from both of these tables in order to get the data. I originally extended the cs_Users table, but then I backed out my changes and went with the cs_UserProfile as it just feels more appropriate for the data I was adding.

The User Entity Object

The first thing you should do is add your new properties to the user object which can be found in “\Components\Components\User.cs” file. Theoretically, you probably can stick with extended attributes, and pull off those values later.  I chose to extend the actual user object for my project. 

[Serializable]

public class User : ExtendedAttributes

{
    {Some CS code here}

    /// <summary>
    /// Store that the user is tied to
    /// </summary>
    private int _StoreAffiliatedToId;

    public int StoreAffiliatedToId
    {
        get { return _StoreAffiliatedToId; }
        set
        {
            _StoreAffiliatedToId = value;
        }
    }
 
    /// <summary>
    /// Store affiliation type – Manager, Sales, etc
    /// </summary>
    private int _StoreAffiliationTypeId;
    public int StoreAffiliationTypeId
    {
        get { return _StoreAffiliationTypeId; }
 
       set
        {
            _StoreAffiliationTypeId = value;
        }
    }

Create Update Delete

Searching the CS code base for cs_UserProfile lands you in the “Data Providers\SqlDataProvider\SqlCommonDataProvider.cs” file in the “CreateUpdateDeleteUser” method. It is very straight forward code that is adding parameters to call the “cs_user_CreateUpdateDelete” stored procedure. So back to the database we go to update that stored procedure. I added two new parameters to the “cs_user_CreateUpdateDelete” stored procedure and then updated the Update and Insert statements in that stored procedure to update and insert my new fields into the cs_UserProfile table. Back in the C# code we now need to update the “CreateUpdateDeleteUser” method mentioned above to take properties off of the user object and put them into parameters to call the updated stored procedure.

public override User CreateUpdateDeleteUser(User user, DataProviderAction action, bool createLocalUserOnly, out CreateUserStatus status) {

{some CS code here}

    myCommand.Parameters.Add(“@StoreAffiliatedToId”, SqlDbType.Int).Value = upm.CurrentUser.StoreAffiliatedToId;
    myCommand.Parameters.Add(“@StoreAffiliationTypeId”, SqlDbType.Int).Value = upm.CurrentUser.StoreAffiliationTypeId;

Read

In addition to the “cs_user_CreateUpdateDelete” stored procedure you will need to update the view cs_vw_User_FullUser to retrieve the new values from the cs_UserProfile table to read the new values.

In the C# code you will also need to update the code that takes the fields from the cs_vw_User_FullUser view and puts them into the user object. The code that does that is located in the file “Components\Provider\CommonDataProvider.cs” in the method “cs_PopulateUserFromIDataReader”. It is very simple code that copies values out of a datareader and populates user object properties.

public static User PopulateUserFromIDataReader(IDataReader dr, bool isEditable, bool includeAudit) {

{some CS code here}

     user.StoreAffiliatedToId = (int)dr["StoreAffiliatedToID"]; 
   user.StoreAffiliationTypeId = (int) dr["StoreAffiliationTypeID"];

The next step is really optional depending on how you layout your data. My extended cs_UserProfile table contains ID’s that are foreign key references into other tables in my system.  In my case I have an in memory cache that map these ID’s to friendly names. You could add another join to the “cs_vw_User_FullUser” view to get your friendly names from your own data if you so choose. Since I have this data in memory already, I updated the code in the file “Controls\User\UserProfileData.cs” in the method “GetPropertyValue”, to lookup the data for these ID’s from my in memory cache. This “GetPropertyValue” method is called from the Chameleon controls which I will use later in this post to show these new values on the UI.  You’ll see references to “ProactiveLogic.WebFacade” which is an external project that manages my cached data.  My new code is noted by the “new code” comments.



/// <summary>
///
Single value control that provides access to a user’s Profile data and links.

/// </summary>

public class UserProfileData : UserData {

protected override object GetPropertyValue(string property) {
   
   
User user = DataSource as User;
   
   
if (user != null && !string.IsNullOrEmpty(property))
   

 
   
switch (property.ToLower())
   

        // Existing CS code 
       
case “signature”
        // Existing CS code 
       
case “signatureformatted”:
       
{
            
CSContext csContext = CSControlUtility.Instance().GetCurrentCSContext(this.Page); 
             
if (csContext.SiteSettings.AllowUserSignatures && csContext.SiteSettings.EnableUserSignatures && csContext.User.EnableUserSignatures)
                 
return user.Profile.SignatureFormatted.Replace(“\r\n”, “<br />”);
            
else
                 
return string.Empty; 
        
}


        // New code
        case “storeaffiliation”:
           
if (user.StoreAffiliatedToId == 0) 
            
{
                 
return “No Store”;
           
}
           
else
           
{
                 
return ProactiveLogic.WebFacade.CachedData.GetStoreByID(user.StoreAffiliatedToId).Name;
           
}
           
break;
          
            // New code
      
case “storeaffiliationtype”:
           
if (user.StoreAffiliationTypeId == 0) 
            
{
                 
return “No Affiliation”
            
}
           
else
           
{
                 
return ProactiveLogic.WebFacade.CachedData.GetStoreAffiliationTypeById(user.StoreAffiliationTypeId).StoreAffiliationTypeName;
           
}
           
break;


       default:
           
return DataBinder.GetPropertyValue(user.Profile, property); 
       
}


    }
   
else
       
return string.Empty;
  
}


// Existing CS code 
protected
override string GetExtendedAttributeValue(string extendedAttribute)
{
   
throw new InvalidCastException(“CommunityServer.Components.Profile does not support extended attributes.”);
}


   


Now the data layer is done! That is all you have to do. When you see the code it is very simple. Onto the UI…


Updating the Default Theme to Acquire and Edit the additional user profile data


I’m going to cut to the chase and show you the code in that I added to the themes to call my new sub forms to handle this new user data. After that I will show the implementation of the SubForms. I added two new fields one is the store the person is affiliated to, and the second is their affiliation to that store (i.e. Manager, Clerk, etc). My store list needs to be filtered by state, so I wrapped this filtering in an ASP.net Ajax update panel.


Getting the new User Properties on User Registration \ Sign Up


In the file “Web\Themes\default\User\createuser.aspx” I added my two SubForms to the Create User Form CSControl. It is very important that you do not put any spaces in your comma delimited list of SubForms, otherwise they will not be properly processed, as I found out.



<CSControl:CreateUserForm runat=”server” SubFormIds=”StoreAffiliationSubFormID,StoreAffiliationTypeSubFormID” >


I then added call outs to my SubForms, and the ASP.net AJAX Update Panel to make SubForms do an Async post pack for the filtering of my dropdown lists. I have cut down quite a bit of existing CS code here to show you only the new code in the context of some of the key CS elements. Keep in mind, the state list drop down is just a filter on my stores, and is not saved.



<asp:ScriptManager ID=”ScriptManager1″ runat=”server” />

<CSControl:CreateUserForm>

<
FormTemplate>
{Some existing CS code here}
{Call out to my SubForms below…}

<tr>
<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
    
    
<ProactiveLogicSubForms:StoreAffiliationTypeSubForm ID=”StoreAffiliationTypeSubFormID” runat=”server” StoreAffiliationTypeId=”StoreAffiliationID”>
    
<FormTemplate>
         
<asp:DropDownList ID=”StoreAffiliationID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationTypeValidator” runat=”server” ControlToValidate=”StoreAffiliationID” Cssclass=”validationWarning“>*</asp:RequiredFieldValidator
     
</FormTemplate>
    
</ProactiveLogicSubForms:StoreAffiliationTypeSubForm>

</td>
</tr>

<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>

<td class=”CommonFormField”> 
     
<ProactiveLogicSubForms:StoreAffiliationSubForm ID=”StoreAffiliationSubFormID” runat=”server” StateListID=”StateListID” StoreListID=”StoreListID”> 
     <
FormTemplate>
    
    
<asp:UpdatePanel ID=”UpdatePanel1″ runat=”server”>
    
<ContentTemplate>            


            <asp:DropDownList ID=”StateListID” runat=”server” AutoPostBack=”true” /><asp:RequiredFieldValidator id=”StoreAffiliationStateValidator” runat=”server” ControlToValidate=”StateListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator>


            <asp:UpdateProgress ID=”UpdateProgress1″ runat=”server” AssociatedUpdatePanelID=”UpdatePanel1″ DynamicLayout=”true” DisplayAfter=”0″ EnableViewState=”false”>
                <ProgressTemplate>Loading Stores <br /></ProgressTemplate>
            </asp:UpdateProgress>


<br />


            <asp:DropDownList ID=”StoreListID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationValidator1″ runat=”server” ControlToValidate=”StoreListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator>


      </ContentTemplate>
      </
asp:UpdatePanel>


      </FormTemplate
      </ProactiveLogicSubForms:StoreAffiliationSubForm>
</td>
</tr>


Editing the new User Properties in the Control Panel


In order to edit the user, I added a new “Store” tab to the edit profile page with a call out to my sub forms in the file “Web\Themes\default\User\edituser.aspx”.  The code is pretty much identical to the create user form, with the addition of a tab. 



<CSControl:EditUserForm SubFormIds=”AvatarSubForm,StoreAffiliationSubFormID,StoreAffiliationTypeSubFormID”>  

<TWC:TabbedPane runat=”server”>

<Tab>Store</Tab>
<Content>
<table>
<tr>

<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
    
    
<ProactiveLogicSubForms:StoreAffiliationTypeSubForm ID=”StoreAffiliationTypeSubFormID” runat=”server” StoreAffiliationTypeId=”StoreAffiliationID”>
    
<FormTemplate>
         
<asp:DropDownList ID=”StoreAffiliationID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationTypeValidator” runat=”server” ControlToValidate=”StoreAffiliationID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator
     
</FormTemplate>
    
</ProactiveLogicSubForms:StoreAffiliationTypeSubForm>

</td>
</tr>

<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>

<td class=”CommonFormField”> 
     
<ProactiveLogicSubForms:StoreAffiliationSubForm ID=”StoreAffiliationSubFormID” runat=”server” StateListID=”StateListID” StoreListID=”StoreListID”> 
     <
FormTemplate>
    
    
<asp:UpdatePanel ID=”UpdatePanel1″ runat=”server”>
    
<ContentTemplate>             

            <asp:DropDownList ID=”StateListID” runat=”server” AutoPostBack=”true” /><asp:RequiredFieldValidator id=”StoreAffiliationStateValidator” runat=”server” ControlToValidate=”StateListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator

            <asp:UpdateProgress ID=”UpdateProgress1″ runat=”server” AssociatedUpdatePanelID=”UpdatePanel1″ DynamicLayout=”true” DisplayAfter=”0″ EnableViewState=”false”>
                <ProgressTemplate>Loading Stores <br /></ProgressTemplate>
            </asp:UpdateProgress>

<br /> 

            <asp:DropDownList ID=”StoreListID” runat=”server” /><asp:RequiredFieldValidator id=”StoreAffiliationValidator1″ runat=”server” ControlToValidate=”StoreListID” Cssclass=”validationWarning”>*</asp:RequiredFieldValidator

      </ContentTemplate>
      </
asp:UpdatePanel

      </FormTemplate
      </ProactiveLogicSubForms:StoreAffiliationSubForm>
</td>
</tr>


</table>
</Content>
</TWC:TabbedPane>


SubForms Implementation    


I created a new project to implement my SubForms in and added a project reference to it from CS. I also updated the web.config in CS to know about my new controls by adding the following:



<controls>
<
add tagPrefix=ProactiveLogicSubForms namespace=ProactiveLogic.CS.SubForms assembly= ProactiveLogic.CS />


Now implementing the SubForms is a little tricky because my SubForms are used in both an update and creation context. In order to do this I always look for “posted” form values first and if there are no posted values, I see if I can get a CS User object to pull the values from for display. My code here is actually doing a lookup based on the state that is selected in order to populate the list of stores in that state. I am only showing my store selection SubForm as it is the more complicated of the two subforms that I created and should give you the most to chew on.



using System;
using System.Data;
using System.Configuration;
using System.Collections;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using CommunityServer.Controls;
using CommunityServer.Components;
using ProactiveLogic.WebFacade;
using ProactiveLogic.Entities;    

namespace ProactiveLogic.CS.SubForms
{

public partial class StoreAffiliationSubForm : WrappedSubFormBase

  private DropDownList StateList;
  private DropDownList StoreList; 

  // this ID is set in the ProactiveLogicSubForms:StoreAffiliationSubForm markup to
  // provide an ID reference to the state list dropdown

  public string StateListID
 
{
   
get { return (string)(ViewState["StateListID"] ?? “”); }
    set { ViewState["StateListID"] = value; } 
  } 

  // this ID is set in the ProactiveLogicSubForms:StoreAffiliationSubForm markup to
  // provide an ID reference to the store list dropdown

  public string StoreListID
  {
    get { return (string)(ViewState["StoreListID"] ?? “”); }
    set { ViewState["StoreListID"] = value; }
  } 

  public override bool IsEnabled()
  {
    return true
  }    

  protected override void AttachChildControls()
  {
    // view state is disabled, so populate the state list every time
    StateList = CSControlUtility.Instance().FindControl(this, StateListID) as DropDownList;
    this.StateList.Items.Add(new ListItem(“<Select Store State>”, “0″)); 

    foreach (State state in CachedData.GetAllUSStates())
    {
      this.StateList.Items.Add(new ListItem(state.StateName, state.StateId.ToString()));
    } 

    // attach the Store list member – the Store list depends on the statelist value
    // which is not bound yet – so we will populate that in the DataBind method 
    StoreList = CSControlUtility.Instance().FindControl(this, StoreListID) as DropDownList
  }    

  public override void DataBind()
  { 

    // determine the selected Store: always use posted form values if they are available –
    // then use the existing user’s values if they are available 
    int selectedStore = 0;
    int.TryParse(this.Page.Request[StoreList.UniqueID], out selectedStore);

    if (selectedStore == 0)
    {
       // if no store is selected yet, try to load the value from an existing user object
       // if it is available
       // (CS passes the User object to subforms if it is available in the context of what the user is doing)
       User ExistingUser = this.DataSource as User;
       selectedStore = (ExistingUser == null) ? selectedStore : ExistingUser.StoreAffiliatedToId; 
       // Select the state for the seleted store
       StateList.SelectedValue = CachedData.GetStoreByID(selectedStore).StateId.ToString(); 
    

    // repopulate the Stores based on the selected state
   
this.StoreList.Items.Clear();
    this.StoreList.Items.Add(new ListItem(“<Select Store>”, String.Empty));
   
    foreach
(Store col in CachedData.GetAllStoresForState(int.Parse(StateList.SelectedValue)))
    {
      this.StoreList.Items.Add(new ListItem(string.Format(“{0} ({1})”, col.Name, col.Town), col.StoreId.ToString()));
    }   

    // select the store – which was either loaded from a post value or
    // user object value above
    if
(StoreList.Items.FindByValue(selectedStore.ToString()) != null
    {
       StoreList.SelectedValue = selectedStore.ToString();
    }
  }


  // CS calls this to get the UI values into the Data object to save 
  public
override void ApplyChangesBeforeCommit(object activeObject)
  { 

    base
.ApplyChangesBeforeCommit(activeObject); 

    User user = activeObject as User;
    if (user != null)
    {
      // Just save the store – the state is just a filter and does not need to be saved
      user.StoreAffiliatedToId = int.Parse(this.StoreList.SelectedValue);
    }
 
}


  public override void ApplyChangesAfterCommit(object activeObject)
  {
     base.ApplyChangesAfterCommit(activeObject); 
  }
 
}
}


Displaying the Data In the Profile


For the displaying the new fields in the User Profile I added callouts to my new fields in the file: “Web\Themes\default\User\userprofile.aspx”


I added my fields under the “GMT” display. These fields query the aforementioned UserProfileData.GetPropertyValue method, based upon the “Property” value.



<CSControl:UserProfileData ID=”UserProfileData2″ runat=”server” Property=”storeaffiliationtype”>

<LeaderTemplate>
<tr>

<td class=”CommonFormFieldName”>
I am a:
</td>

<td class=”CommonFormField”>
</LeaderTemplate>

<TrailerTemplate>
</td>
</tr>
</TrailerTemplate>

</CSControl:UserProfileData>

<CSControl:UserProfileData ID=”UserProfileData1″ runat=”server” Property=”storeaffiliation”>

<LeaderTemplate>
<tr>
<td class=”CommonFormFieldName”>
of the Store:
</td>
<td class=”CommonFormField”>
</LeaderTemplate>

<
TrailerTemplate>
</td>
</tr>
</TrailerTemplate>

</CSControl:UserProfileData> 


Displaying the data in Forum Posts


Forum Flat View


The flat view of a forum is contained in the file: “Web\Themes\default\Forums\thread-flatview.ascx”

Under the line that has the user Avatar, I added a callout to my new fields.  Notice how easy it is to display your new fields. 



<CSControl:UserAvatar runat=”server” BorderWidth=”1″ Tag=”Li” CssClass=”ForumPostUserAvatar” />

<CSControl:UserProfileData ID=”StoreAffiliatedTo” runat=”server” Property=”storeaffiliation” Tag=”Li” CssClass=”ForumPostUserAttribute” />

<CSControl:UserProfileData ID=”StoreAffiliationType” runat=”server” Property=”storeaffiliationtype” Tag=”Li” CssClass=”ForumPostUserAttribute” />


Forum Threaded View


The post view for the threaded view of a forum is in the file: “Web\Themes\default\Forums\post-threadedview.aspx”

I added my new fields under the display name.  Notice that while the Forum flat view uses a list to display the user values, the threaded forum view uses container divs. 



<div class=”ForumThreadPostAuthor”><CSControl:UserData runat=”server” LinkTo=”Profile” Property=”DisplayName” /></div>

<div class=”ForumThreadPostPubDate”><CSControl:UserProfileData ID=”StoreAffiliatedTo” runat=”server” Property=”storeaffiliation” /></div>

<div class=”ForumThreadPostPubDate”><CSControl:UserProfileData ID=”StoreAffiliationType” runat=”server” Property=”storeaffiliationtype” /></div>


Conclusion


I spent about 8 days @ 1.5 hours a day to come up with what is in this post. It took another 3.5 hours to write this post (edit: about 3 more hours to format and revise the post). I’m hoping that this helps you extend CS much faster than that. I basically just loaded the code and had at it. I’m very open to suggestions on how to improve this, but I wanted to get this posted because it works for me and should hopefully help some of you who want to dig in and extend CS. I think CS seems to be a great platform and I am very impressed with how factored and clean the code is. The extensibility at the data layer seems to be an area that can be improved, but now that I typed up these instructions, hopefully it is a day of work or less to extend the CS schema so that you can capture, view and query your user data efficiently.


Cheers,
Jon