Monday, August 28, 2006

Community Server Customization - Expanded Member Search Part 2

In part 1 of this subject I explained the basics of how to change the skins and controls to allow a custom member profile field to be searched from the user/Members.aspx page. In fact, according to CS' Daily News, perhaps I said too much about various files and controls without giving an overview of what I was trying to accomplish. To summarize the intent:

  1. Additional fields have been added to the user profile in the database. This required a change to the view cs_vw_Users_FullUser in order to expose the property. My example is silly, but sufficient - Eye Color is now a profile property.
  2. We want to search by Eye Color as a distinct user property on the Members list page. We modify the search control on this page to include a drop down with various eye colors on which to search.
  3. When the search control receives the request, it must pass the selected eye color in a query string which will actually drive the database query values. In order to pass this value along to the , we added the property "EyeColor" to the UserQuery object.

With this level of change there are a number of classes which must be modified. I suggest you try very hard to work with custom versions of the classes in order to avoid upgrade problems and a later inability to find the custom code you wrote. In order to make this customization I have created the following classes in my custom assembly:

  • UserSearch - extends CS.Controls.UserSearch and implements OnInit, SearchButton_Click, GetUsersAndBindControl, and AttachChildControls mainly as copies of the original. I only call the base in OnInit after initializing a local copy of _isAdmin.
  • ExtendedUserQuery - extends CS.UserQuery and adds only the single property value I wish to store.
  • CustomCommonSqlDataProvider - extends CS.Data.SqlCommonDataProvider and overrides only the GetUsers method, along with a constructor that simply passes its parameter values to the base. Don't forget this will require a provider reference change in communityserver.config under the providers node where the name = "CommonDataProvider".

One caveat here where I broke my "customize it" rule - you would need to create a custom ForumMembersView as well in order to call the GetUsersAndBindControl on your UserSearch control because this method is not virtual. I decided to live dangerously and changed the base class to make this method virtual - but you could take the high road.

Now, I described what I did in UserSearch last time. The changes required in the common data provider will fill out the rest of the story. If the query passed into this method is of the type ExtendedUserQuery, then we will need to generate our own member query clause (if not, we can just pass the work onto the base). The member query clause is a generated sql clause executed by the stored procedure "cs_users_Get". This is a good thing because it will keep us from modifying the stored procedure. In the provided method this clause is generated by the static method BuildMemberQuery on the SqlGenerator class. We'll need to copy this entire class to our custom code, and change query parameter to accept an ExtendedUserQuery (you can't extend the class or get at its oddly protected static helpers).

We want to add our modification to the where clause, so look for the comment "// ORDER BY CLAUSE" in this method as it indicates the end of the where clause creation section where we will insert our new predicate. The where clause will have at least one predicate already, so our clause will start with " AND". The profile properties are exposed in the view mentioned above, cs_vw_Users_FullUser which has been given the alias "P". Thus, our predicate format is:

" AND P.EyeColor = '{0}'"

Finally then our new sql generator code is:

if(query.EyeColor != null)
{
    sb.AppendFormat(" AND P.EyeColor = '{0}'",query.EyeColor);
}

Once you have done this, the rest of the code in the GetUsers method remains unchanged. Finally, you should copy the static method cs_PopulateUserFromIDataReader into your provider too in order to put the custom values into the created User object before passing it back. However, it isn't necessary to get the user search results.

I said previously that it would be even better to make a more generic property search mechanism. In order to do that we change the ExtendedUserQuery to have a StringDictionary rather than an individual property. For the query string you can either ignore it and pass values via another mechanism such as user Session, or add a delimited set of strings to a Property Name value such as ppn=EyeColor.Height.Weight and a Property Value value such as ppv=Blue.74in.175. Then your sql generation code would just need to iterate the values and add predicates as necessary.

Even better would be an enhancement to CS that used a more flexible means of manipulating the query. I am biased, but I really like the WhereConstraint idea in Hydrus' DataSetToolkit technology.

Submit this story to DotNetKicks

Tuesday, August 22, 2006

Community Server Customization - Expanded Member Search Part 1

You can search several user parameters in Community Server in the default implementation of the UserSearch control, but if you have added some custom attributes or fields for member profiles, then you'll need to customize the search. In this case, I am not just talking about customizing the UI to support the params, but the search functionality itself.

The default search functionality is carried out in the UserSearch control, which is displayed on the ForumMembersView control from the Discussions namespace (Note: the UserSearch control shares some functional similarity to the CSSearch (SearchBarrel) implementation, but is distinct).  When you click on the Search button the page posts to itself, turning your query into a set of query string parameters. The existing parameters are as follows:

  • 'Search=1' = Perform Search
  • t = Search text
  • st = search type to perform (for admins only) [search by username or by email; provides values of all/username/email]
  • 'su={0}' = search by username (boolean 1 or 0)
  • 'se={0}' = search by email (boolean 1 or 0)
  • 's={0}' = include accounts with active or inactive status.
  • r = role to limit search
  • jc = join data comparer (gt/lt, etc)
  • jd = join date
  • pc = post date comparer
  • pd = post date
  • sb = sort column
  • so = sort by order (ASC/DESC)

Specifically, the ForumMembersView control populates it's user list during data binding by calling GetUsersAndBindControl on the UserSearch control referenced by the view.  Within the UserSearch control, searches are turned into query strings which in turn are turned into a UserQuery object which is eventually passed to the static Users.GetUsers function to populate the list of users found by the query.

In order to tweak this functionality we'll need to customize the SearchButton_Click event handler and GetUsersAndBindControl functions on the UserSearch control. These methods aren't available to be overridden, so we'll need to copy them into our new class that extends the provided class, and we'll change the skin/view to include our control instead of the CS provided control. Unfortunately, we can't allow the original class to receive the button click event because it redirects the response, so we'll need to override and copy the contents of AttachChildControls (where the event is wired up to the search button) as well. This is a common problem in implementing sub-classes of CS controls that could be solved by setting the event handlers as protected virtual members.

In any case, once we have our custom UserSearch control (not yet customized) we need to add field selectors to the UI of the Skin-UserSearch.ascx skin, and update the View-ForumMembers.ascx skin to reference our UserSearch control instead of CS'.

Having added the search selector fields, we need to update the SearchButton_Click event handler to add our new fields to the query string. To do this, choose a moderately descriptive shorthand for your item that isn't already in the list above; ie. 'ec' for an "Eye Color" attribute. Likewise, you'll need to modify the GetUsersAndBindControl method to recognize your new attribute in the query string, and add it to the UserQuery object. You will need to subclass UserQuery and add your custom properties directly to the class. 

Your query-string writing code in SearchButton_Click might look like this:

if(this.eyeColorSelector != null && this.eyeColorSelector.SelectedIndex != 0)
{
    url.AppendFormat("&ec={0}", HttpUtility.UrlEncode(this.eyeColorSelector.SelectedValue));
}

And, your retrieval code in GetUsersAndBindControl might look like this:

string eyeColor = context.QueryString["ec"];

if(!Globals.IsNullorEmpty(eyeColor))
{
    query.EyeColor = eyeColor;
}

Next time let's look at the details of updating the data provider to use the new query value, and a more flexible way to handle multiple custom attributes without adding every one to the UserQuery.

Submit this story to DotNetKicks

MS Betas I Like

Looks like I can finally get back to Windows Desktop Search with the new v.3 beta that includes x64 support:

http://www.microsoft.com/downloads/details.aspx?fa...

I am also really enjoying the new blog writer application, and am using it right now...

http://download.microsoft.com/download/f/...

Submit this story to DotNetKicks