Monday, September 24, 2007

Earned Value Early Results

We have our first reporting period under our belts, and I am very excited about the EV results.

In this fictionalized representation of our first project, we see the period % complete reporting by the team leader:

This evaluation of our status leads to the following period values for EV:

You'll notice that while 145 hours were spent instead of only 120, we also saw 8.2% EV, instead of the planned 6.63%. However, the question we have is, was this enough extra EV to justify the hours spent? In our calculations we have the answer:

We're still all green because the EV was indeed enough to overcome the overspending. Notice that the CPI is roughly $1.02 - this means that the EV just barely covered the cost overrun for the period (we got $1.02 effort out of our team for each project $1 planned). Thus, while we spent over $17K in the period on labor instead of just over $14K, the project is projected to end early, and still come in under budget. What this also shows, is that we'll need to watch this team's hours closely over the coming weeks to make sure this level of efficiency is maintained.

Our second team had a more modest hours overage, but still managed a healthy start on EV:

Leading to an even rosier bottom line report:

So far, this seems like a realistic view of real project progress. Obviously, there's still a ways to go. BTW - it didn't take even close to 15 minutes to enter and analyze the data.

Submit this story to DotNetKicks

Friday, September 21, 2007

Earned Value Management

One session of a conference I attended way back in March was a discussion about managing projects presented by Juval Löwy. One of Juval's excellent claims was that he had used earned value analysis to keep his management time to 15 minutes every Friday afternoon. He explained he used the rest of his time to become a better architect, and when he was finally too bored with the simplistic task of management, he moved on to architecture.

A great story, and certainly a compelling one for someone that needs to manage on less than 40 hours a week. I have been waiting to use this magical method, so as a recent project was ramping up, I pulled out my notes and hit a few websites to try and prepare for the 15 minute management job. Going on scant examples of exactly the type of project analysis I wanted caused this task to take somewhat more than 15 minutes. I found some helpful examples here and here.

If you aren't familiar with EV, the basic idea is that actual work is tracked against the amount of value accrued by the work completed against the plan of value to have been accrued according to the plan milestones. The notion of Earned Value has to do with tracking accomplished activities, rather than just time spent (I think this works well with my management objectives - I don't care how much time you spent, I care that the work gets done). Each activity has some percentage of value to the overall project. A simple way to get at this in software development is to divide the hours for one task against the total hours estimated for the project. Choosing the proper level of task granularity to track is a matter of balancing ease of reporting with meaningful levels of detail for catching problems before they go out of control. Once the earned percentage values are assigned to milestone tasks, you can either work with dollar figures or just work with hours - it doesn't matter. The important thing is to calculate how much earned value should be accrued at various key intervals along the project path. For instance, if I am checking in on my developers every Friday night, then I calculate EV earned to each Friday. When my developers report in, they tell me how many hours they worked this week, and what % complete they are for each milestone task.

Planned EV will be the number of developer hours available in the time period divided by the total number of project hours. When each developer reports a percent done, you multiply that by the percentage EV calculated for each task against which you are reporting. Summing each EV gives you the period EV which you compare against your goal. You can then use the total actual hours reported as a measure of how much real effort it took to get actual EV. This calculation comes into play as you forecast the future effort expenditure of the project. If you thought you could complete 10% EV with 80 hours, and got 8% EV with 80 hours, your SPI will give you a multiplier for estimating future period effort. This is where the 15 minutes comes in. Each Friday you must inspect your planned EV and and planned Costs against your actual's. You then analyze what variances in the charts mean to you:

  •  PEV way ahead of AEV? You underestimated and should add resources
  • AEV way ahead of PEV? - You overestimated and may be able to divert resources
  • AEV=PEV but costs overrun? - You can meet the deadline, but your resources are inefficient - you need to figure out what's bogging down your resources.
You get the idea. Manage next week's resources based on your 15 minutes of EV and cost analysis.

Without a tool to provide EV analysis for me, I turned to Excel. The only gotchas I had in setting up the calculations was discerning the best path for tracking - either project to date sums, or per period values; and getting graphs to show the EV and cost trends. In the end I went with per period values and some extra calculations to show trends and two separate graphs for EV and Cost.

With the plan in place, I simply communicated the project milestone tasks to the team along with target dates and requested weekly reporting of progress. Hopefully this leaves the devs with schedule freedom and a sense of autonomy, while giving me a way to know which adjustments are necessary before we have a big problem.

Submit this story to DotNetKicks

Orcas Beta 1 and other new stuff

It seems to me that the development world is experiencing another period of rapid evolution again, much as it did in '99-'01 around web application servers, XML, and all the XML spinoff technologies. Interestingly, during the last evolution many people left javascript in the dust; but it has had a massive resurgence to the forefront this time around. I think it also interesting to note that the lastest tools and technologies are clearly evolutionary in that most are still new ways of utilizing the XML breakthrough. Well, I am sure someone more knowledgable wrote about this years ago; but it's clear that it is time to dig deep into the recent mountain of information.

As one who is primarily an MS-based developer, a lot of that mountain I am facing is coming out Redmond lately.

  • .net 3.0
    • Windows Communication Foundation
    • Windows Workflow Foundation
    • Windows Presentation Foundation
  • Silverlight
  • C# 3/3.5 (lambdas, extensions, implicit types, anonymous types, type initializers, expression trees, and LINQ)
  • AJAX Framework
  • PowerShell scripting
  • Vista/Longhorn Server (IIS7 being the big one here so far)

Along with this has been the birth of Ruby and the resurgence of javascript.

Of course, many of these things have been around or brewing for years, but the pressure has mounted as more of the MS stuff nears release.

I've had the opportunity to play with Orcas the last couple of days, and it has been fun. Silverlight programming isn't really something I think I'll do a lot of, but it provided a good way to mess around with XAML and learn about what's available. It also provided a way to get into VS 9, and the new Blend tool.

First, the XAML stuff is cool, especially the web delivered Silverlight. Updating markup, and then watching visuals automatically update was great. However, my artistic skills approach those of a first grader so the appeal is limited. I can see a potential upside in using Silverlight for rich administrative interfaces. I hate all-flash sites because they are usually more about style than substance (i don't need to hear sounds when I drop down menus or wait for transition effects when I go to a new page), but if used carefully and mixed with AJAX it could be good.

VS 9 has some fairly exciting innovations too. I especially like the javascript editor has improvements (though it could be a lot better - see Aptana). 

C# 3.5 has some great stuff going on. Scott Guthrie's coverage of LINQ features has been excellent. It does take some time to get used to the LINQ programming model (which utilizes most of the new language features), but it's well worth it. Start investing the time now.

Submit this story to DotNetKicks

Tuesday, June 05, 2007

Private Workspace Config Mgmt w/Team System

On a recent project our team has been using Microsoft's Team System from within Visual Studio. We are using the built-in capabilities for:

  • Requirements Definition
  • Task Assignments
  • Source Control
  • Build Server
  • Bug Tracking
  • Code Coverage
  • Unit Testing

So, we're involved in it up to our necks. We've been using pieces of the system for quite a while, but this is our first immersion. The SCM is a little funky sometimes, and really annoys my SCM-geeked teammate.

Honestly, I think it has been a pretty good experience overall. I like the reporting we get out of the box (these only come into play if you do use TS for everything), and there is no question that as an extension of the IDE, having everything available in one familiar place is a big bonus.

The main point I wanted to bring up here is an early issue we had with getting our private workspaces* and build server to have all the right files from the repository. We do not share an overall folder structure, and I don't like forcing developers to have their files in any one place on disk on their machine. I am just too much of a libertarian for that.

Because this project included a couple of frameworks, we decided to install those on every machine, and depend upon the GAC for references. However, we wanted to share a signing key, a shared AssemblyInfo file, a test run configuration file, team build types, along with multiple assemblies not included in the installed frameworks.

For the shared files we decided to use the solution items folder for the solution. In order for the items here to be source controlled, and therefore downloaded from the repository, they had to exist beneath the root workspace folder. We decided that we would mimick the folder structure exactly on disk as it was in the solution items folder. Specifically, we created solution items folders, and then created their counterparts in the root solution folder on disk.

Assemblies were placed in an Assemblies directory and other files in a Common folder (unfortunately, this is still one area where the SCM can still be flakey and occasionally get latest version doesn't seem to pull new solution items).

Now, in each project we referenced the assemblies in the solution items folder rather than locally. We also needed to put links for the common key and sharedassemblyinfo.cs into each project. In order to do this, each csproj file was hand edited with a new "Compile" element:

<Compile Include="..\Common\SharedAssemblyInfo.cs">
     <Link>Properties\SharedAssemblyInfo.cs</Link>
     <Visible>true</Visible>
</Compile>

The Link sub-element tells the project where to display this file relative to the project root.

We added a "None" element for the key:

<None Include="..\Common\ourkey.snk" />

Finally, in order for the .testrunconfig file to work, and to see the team build files they had to be included in the solution via the context menu 'include existing item' - for some reason these references did not come along with updated versions of the solution file.

With this all in place, every developer can reliably get latest and build without issue. 

*I mean this in the sense of the Configuration Management pattern given by Berczuk which I think is probably normative for most developers - do your dev in a private workspace where you control the versions of the components you are using and the version of the code you developing against. You control when and how your environment changes. This is basically done by downloading a complete environment from the repository to your local workstation.

Submit this story to DotNetKicks

Generic Mock Factory

I've been involved in VS integration work lately, and within the unit testing framework provided by the integration SDK is an excellent mock factory for any interface based type.

If you are involved in unit testing where you need mocks, this is an excellent generic resource for any project - not just VS integration.

GenericMockFactory.cs

Submit this story to DotNetKicks

Saturday, March 31, 2007

Windows Communication Foundation - SOA

I have spent time this week learning about the WCF and it's place in jumpstarting SOA based application development while attending the SDWest Expo in Santa Clara. Juval Löwy provided most of the presentations on WCF; along with a few by his chief architect Michele Bustamante.

One of the main points Juval made was that WCF based development is an evolutionary shift from Component Oriented development to Service Oriented development. During an excellent history lesson on the growth of software development methodologies he made the case that services are the next major shift because they improve upon the goals of component development. The WCF improves primarily in decoupling: technology from the service provided; transaction and concurrency management; security; reliability, versioning, and communication protocols from the business logic. Because the WCF provides this great level of decoupling Juval declared with a mischevious smile, that coding in the CLR is coding in the stone-age. The WCF has arrived, and everything you code should now be coded as a service.

While we can't take this declaration entirely seriously - Mrs. Bustamante is more balanced -it is a fact that transaction support, protocol independence, security, reliability, versioning, and concurrency are provided for free by the WCF. Obviously all services decouple the technology and platform from the service provided - that's why the industry is in love with services - but the WCF adds so much more.

All this goodness is provided by implementation of the WS-* specifications by the WCF. That really makes it so much better in my opinion because it means that transactions, security, etc. can pass from a WCF service to any other which also implements those standards (IBM was part of the spec group, so I am sure their products will follow suit soon). You have to take some time to think about what this means to the way you can build software - we may have to agree with Juval that a fundamental shift is underway. This isn't about WCF causing a fundamental shift, SOA causes the fundamental shift. It's the easy use of the full range of WS-* standards which makes the WCF such an attractive reason to start thinking services more often than you ever have before - even if not for absolutely everything as Juval suggests.

If you aren't familiar with the WCF, the basics are very simple. The new ServiceModel namespace includes an attribute for ServiceContract which is placed on an interface, making that the contract for the web service. Each operation within the web service interface is attributed as OperationContract, exposing these members of the interface on the service. Every class type exposed in an operation signature must be attributed with DataContract to mark it as serializable. Given these few attributes, your classes are ready and you just need a little configuration. If you remember .net Remoting, then I suggest you forget it, but this will at least be familiar to you. I won't go into here, you can check out WCF configuration here.

If you are going to design more services, you have to take some things into account during design. I won't discuss the normal service design issues that exist in all services. However in WCF programming a couple of things got my attention. For instance, while you cannot expose an interface type as a contract operation variable, you can expose "well known types" which will enforce an object hierarchy. This will be slightly different from the pure OO interface and class design you might have used, but preserves the basic idea.

Also, when it comes to passing state data an object DAL is enforced by classes attributed as DataContract. This ensures serialization capabilities by the Framework. As a design consideration it simply means you have to have classes that hold simple data elements to pass across the service boundary. This isn't a bad thing, it just may be different than you would design for internal business objects. Mix with LINQ (in the near future) within your .net classes and you'll probably have these pure data objects in your design anyhow.

One final consideration concerning services everywhere is the issue of performance. Obviously, if you are going across the wire as SOAP/HTTP, you're subject to the inefficient payload as well as the potential wire latency. This is no different from any other service call. But, with every service call there is a serialize/deserialize operation for every object. I haven't tested this hit, but it will be something compared with the nothing of in-memory object passing.  So, if you are configured for SOAP/HTTP performance is already a potential problem and you'll never notice the performance of any other part of the WCF infrastructure code. If you are going binary/TCP, then the test would be worth the double-check.

Finally, if you are implementing WCF the folks at iDesign have a whole bunch of helper classes and utilities you should check out.

Submit this story to DotNetKicks

Tuesday, February 27, 2007

Helpful C# Class File Template Change

Something which has annoyed me several times in VS2005 is that new classes are internal by default. I almost never want an internal class, so I went looking for the option to change. I never did find it, but a colleague has just pointed out the solution:

All new files come from various files in %InstallDir%/Common7/IDE/ItemTemplates

Find Class.zip and modify the class.cs file in this archive. Be sure to delete or modify the contents of the ItemTemplatesCache folder while you are at it.

*InstallDir is [Program Files/Microsoft Visual Studio 8] by default.

Submit this story to DotNetKicks

Friday, December 15, 2006

Community Server Paged Permissions Grids

The default implementation of Community Server assumes you will have only a handful of roles. This caused me some problems when I added 700 new roles to configure specific admin/user permissions on 350 forums. All of the permissions grids in the control panel are not paged, and neither IE nor Firefox like loading the massive permissions grids into memory.

Thankfully, the answer is quite simple. In order to add paging to all permissions grids in the control panel, you simply make a couple of minor changes to the BasePermissionsGridControl.cs file.

At the bottom of the buildGrid() method you'll see a comment concerning paging just after the call to ApplyUserSettings(). You can delete each line from that point forward since we now want paging.

Next, you need to add an override of ConfigureGrid and add the following single line to the method:

AddGridPagerTemplate("Name");

Finally, set the Grid.PageSize property to whatever size you like in the BasePage_Load method (or leave it alone and let the default take over).

Viola! All permissions grids are now paged in the Control Panel.

Submit this story to DotNetKicks

Tuesday, December 12, 2006

Exchange Distribution Lists and WebDAV

Download Sample Code

I encountered an interesting side project recently where I wanted to retrieve information about distribution lists on a local Exchange Server from a web-based application.

I didn't want to use CDO/MAPI directly for various reasons, so I looked into the WebDAV api available for querying Exchange via web requests. I found some helpful information here, and was quickly able to produce a class to consume distribution list data for use in my app.

The basic query for retrieving all distribution lists from a particular folder (where folder is the RootURI below) is:

<?xml version=\"1.0\"?>

<D:searchrequest xmlns:D = "DAV:" >

<D:sql>SELECT "urn:schemas:contacts:cn\", "urn:schemas:contacts:members\", "DAV:displayname" FROM RootURI WHERE "DAV:ishidden" = false AND "DAV:isfolder" = false AND "DAV:contentclass" = 'urn:content-classes:group'</D:sql>

</D:searchrequest>

DAV content-classes are found via mappings from PR_MESSAGE_CLASS and PR_CONTAINER_CLASS properties. The schema references to specific properties in Exchange are derived from the Exchange Server schema. In the above example, we want the property cn from the urn:schemas:contacts namespace. The uri we query should contain contact items whose content-class is of type group.

Now, in order to figure out where all these properties live, and what you can expect can be challenging. You can start with the properties reference by namespace on MSDN. These will tell you the content-class. Or, as I did here, you can use a tool like IndependentDav to get a look at what is returned from the server. I don't think the tool is necessary once you have a handle on where things come from, but it can difficult to track some things down because mappings between Exchange Store schema content-classes and DAV content-classes are not 1:1 and the properties reference gives the content-class of the Exchange Store schema. Finally, you can query the store at a uri without limiting the return values, and look through the query results manually.

Getting on with the distribution lists then, I created a helper class to make the requests and manage the two part process of first constructing a DistributionList class, then filling the members of the list with a seperate query. I pass the streamed response from the query directly to the constructor of the DistributionList class. The code is long and cumbersome, so it's better to just look at it in the sample.

The list members are actually not found via webDAV directly, but a query to the OWA server services as described here. I created a ListMemberList class to hold ListMember classes. The constructor on the list parses each member in the response, and set it as a property on the DistributionList class.  

When its all said and done, I can use the management class to get the DL's from the configured server with very little overhead, and - best of all - no interop.

Submit this story to DotNetKicks

Monday, November 20, 2006

Enable Domain Account As App Pool Identity

While the answers are around already, the simplified listing of what to do when you create a user in your domain to run an IIS 6.0 application pool on Windows Server 2003 is:

  1. Add the user to the IIS_WPG on the web application server.
  2. Run aspnet_regiis with the -ga flag and the domain qualified user name (ie. MyDomain\TheUser). This gives the app pool user the ability to read and write from appropriate files, etc. I thought this would be accomplished by adding the user to the group above, but it didn't work until I had completed both of these steps.
  3. Run setspn on the domain controller (you'll need to have the toolkit installed) with the protocol name 'HTTP' and application server name (ie. HTTP/myAppServer) and a second argument for the domain qualified username (ie. MyDomain\TheUser). All together that is 'setspn HTTP/MyAppServer MyDomain\TheUser'. It is also recommended that you run the same command again, but with the fully qualified name of the app server.

Some security minded folks will tell you that you shouldn't actually do this anyhow, but if you must this set of instructions will allow you to use a user with minimum permissions. Just make sure this user is not used for anything else - thus limiting the potential the user will have an increasing permission set over time that no one can remember.

Submit this story to DotNetKicks

Thursday, September 07, 2006

Using Sandcastle

You Mean I Actually Have to Use Sandcastle? Yes, with the death of NDoc, if you want to generate a chm based on triple-slash comments then you will probably need the help of Sandcastle.

It was nice of MS to release this thing - really. It would have been nicer if they would have sent a team to help NDoc, but let's not dwell on what could have been.

Using the August CTP I quickly found myself in hell. It was one thing to follow the directions for the sample. It was a whole other thing to actually build docs for my own projects. I created a bit of the hell for myself by trying to build docs in the directory structure of the project for which the docs are meant to be a part. I am sure I am not alone in this desire - it makes source control and packaging so much simpler.

I'll skip to the end and let you know that the results are quite nice - I am glad to have the docs. Everyone acknowledges the problems, and in fact there are some great helper articles referenced in the comments - mostly here. Heck, one guy even created a nice batch file and configuration doc builder. Unfortunately, it doesn't actually create usable batch file - at least not for me. It was faster to fix the batch I was working on than to debug his tool. That tool would be a great thing to spend some free cycles on and fix - but then look what happened to NDoc, so we can be sure that will never happen.

Another guy created a serious batch file here - again I couldn't make it work for me.

There are a few morals to this story:

  1. 1) Batch files and esoteric config files without good docs are always hell to work with. Be prepared to dig in.
  2. 2) Just hard code all the folder/file references in the sandcastle.config to full paths. You can try to be all fancy, but this is just a CTP and there are more robust versions to come. Besides, it's just install locations and that isn't too bad (in fact, the config doc creator tool mentioned above actually does a nice job on this).
  3. 3) Create all the content folders (html,scripts,art,styles) directly below the batch so BuildAssembler doesn't get confused
  4. 4) A much discussed batch line from the examples where the results of one transform are piped into another XslTransform statement is total BS - it never worked until I split the line into another statement with results from the first. Not sure why someone felt they needed the pipe to begin with.
  5. 5) Don't try to name any help doc transform results to anything other than test.[hhc,hhp,hhk]. I don't know anything about the hhc API, but it sure doesn't like to work with anything other than test. Just rename the output, test.chm, to something else at the end of your batch file.
Submit this story to DotNetKicks

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

Monday, July 31, 2006

Overview of Customizing Community Server

I have been learning a lot about customizing CommunityServer lately and it occurs to me that it would have been easier to do more faster if I had had an overview of the way CS is built. To that end, I will try to codify what I have observed.

Structurally, CS can be a little confusing because it appears to be a fully implemented asp.net 2.0 website application. The fact is that it is almost entirely 1.1, but was built by people who were very knowledgeable about the changes coming in 2.0. Whether its the use of master pages and skins, or the appearance of a global IsNullOrEmpty string checking method - the app seems to be 2.0. It isn't; the 2.0 version still uses the home-grown skins/master pages and many other 2.0-seeming features.

In terms of customizing the way your website looks or behaves you have to start with the aspx pages found in the various folders of the website application structure. These pages will point you to the various skins or controls in use. Yet, they will never (besides in controlpanel) point you to the code or visual features of the website. All of the real implementation occurs in the master pages, skins, and views, or in the control code in one of the included assemblies.

Each aspx page will identify some master page (not identifying one explicitly means it will use master.ascx) in its CS:MPContainer control declaration, and potentially one or more control declarations in CS:MPContent controls. The master pages are generally slim, and control overall layout of a page. The actual master pages are simply custom controls (ascx files) located in the /Themes/[current theme]/Masters folder of the website application. The most common base implementation of a master page is of 3 sections called 'lcr' (left side content), 'bcr' (body content, and 'rcr' (right side content). You can either define controls for every descendent page in these sections in the master page, or override master page content by declaring CS:MPContent controls with these ids on your aspx page. Skins should not implement these content controls. For example, if you wanted to understand how each thing is showing up on default.aspx, you would open default.aspx and find the 'ThemeMasterFile' attribute on the page level CS:MPContainer control. If you navigate to the HomeMaster.ascx file you'll see that the only thing being added here are some style includes in a "HeaderRegion", and the 3 content sections. In order to really understand where the content is coming from, you have to look at the controls declared within the various content sections.

A control declaration on a page or skin will carry a custom prefix defined on the page and the name of the control - this is standard asp.net customization. It is important to note such control names because first - along with the prefix declaration on the page - it will point you to the control code in the proper assembly where you can see how it is implemented. Second, the name is almost always identical to the the name of the skin that is used to display the control, with an added prefix of 'Skin-'. By default, all templated controls in CS will load a skin named "Skin-" + [the name of the control] + ".ascx".

While the master pages generally declare overall sections in which the various skins will be displayed, the aspx pages, skins, and views define the HTML and any additional sub-controls along with client or server side script to control display. I believe it is preferable to leave all html out of the aspx pages, and rely on the skins for this implementation. As a simple example, if we look this time at 'login.aspx' we'll see that the master file is not declared (so it is master.ascx) and all content is controlled by the "CS:Login" control. That means this control is declared in the CommunityServer.Controls.Login class, and its layout will be found in /Themes/default/Skins/Skin-Login.ascx. Sure enough, the layout of the login page is on this skin. The functionality (application logic) of this page is found in the class file.

This brings us to an important lesson about how all this comes together in the application: the class files control behavior by "wiring" properly named controls to certain events or operations on the back end. For instance, the DefaultButtonTextBox control for the password on Skin-Login.ascx must be named 'password' in order for the control logic to work properly. This magic takes place in the "AttachChildControls" method of each control which manipulates its members on the back end.

Using this basic knowledge we can then start to change how our website looks and behaves. Each templated or skinned control (those with skins) has a property "SkinName" which it inherits and will consult as the proper skin to apply if it has been supplied. Recall that if this property is null, then the skin named "Skin-[control name]" will be applied. Note that I have run into controls which ignore this property, but it is not the norm. As such, if we want to change how login.aspx looks we should create a new skin, and provide the name as a "SkinName" attribute on the control declaration on login.aspx. I think you should copy and rename the skins rather than alter them because it will save you headaches later if you try to upgrade CS, and clearly shows where you have made changes. When you fill in the "SkinName" attribute you use the full file name of the skin you created. This name may need to include the sub folder when you are dealing with blogs and galleries (i don't really have the nuances of these exceptions mastered but generally the controls from these assemblies automatically determine the folder which contains their skins so try that first. Aspx pages in the Blog subfolder are really an exception to most things I have said so far anyhow and I'll cover that later).

If you want to change the way login.aspx behaves you'll need to modify the Login class. Again, rather than modifying the class provided with CS, you should create a new assembly for your controls and extend the Login class via inheritance. You can change the name to match your modified skin if you have one, or leave the name the same. The only name collision issue I had working in VS 2003 was with the namespace including the CommunityServer.Controls prefix (so don't use MyCompany.CommunityServer.Controls, try CS.Controls) - the controls themselves are all fully prefixed in the aspx and ascx pages so there is no confusion there. I have found that the control classes aren't all designed real well for extension, so I often have to copy base class methods in order to modify behavior, but I am trying to guarantee that an upgrade will still work, and that I know where my code begins and CS provided code stops. Once you have your class built and the assembly included in your web project, you can change or add the TagPrefix declaration on your page and repoint the control declaration to your new custom control.

Blog Skin Exception

Blog aspx pages generally declare which view they are using, rather than a skin. These "Views" are found in /Themes/Blogs/[current blog theme]/Views and have the name "View-[view name].ascx. Views contain layout templates with various blog controls in them. Each control then has a skin named the same way I mentioned above for other controls. However, these controls have their skins in the /Themes/Blogs/[blog theme]/Skins folder, rather than with the other skins.
Submit this story to DotNetKicks

Thursday, June 29, 2006

Community Server Customization: Title Bar Links

I have really enjoyed learning how to use and customize Community Server. I think it is a great platform for developing community based websites. Yet, it can be difficult to work with because customizations aren't documented well. To that end, I will try to share things I have learned while customizing CS. However, my disclaimer is: I just started this type of CS customization, and while I have searched for the best way to do things, I might have missed a better way.

The basic problem I am writing about is that I have a page which is not one of the base "areas" on install (home/blogs/forums/picture/files, etc) but which I want to link to from the title-bar. I not only want a link, but that link should be highlighted when a user is located on the custom page, or in the custom area. The link should also be relative from any location from within the site, no matter if you are hosted at the root of a domain, or under a virutal directory (the kind of reference you get with the tilde in a asp:hyperlink navigateUrl in 2.0).

Link magic occurs in the SiteUrls.config configuration file. To accomplish our goal, we'll only need to edit the config file - soon I'll write an article on customizing reading the config. There's a whole lot you can do in this file, but everything is accomplished through three types of entries:

location - A location has an attribute for 'path' which is a root-relative path to a particular location in the site. For instance the weblogs location path is "/blogs/" because this is where all blog related files are located. There is also a name attribute which will identify a particular location for later reference. Finally, you can set a boolean attribute "exclude" to indicate whether or not this location is excluded from url re-writing (url re-writing is a process by which paths are canonicalized or formatted according to patterns in siteurls - possibly other stuff. You can even setup your own rewriter via the provider pattern supported for most of this type of low-level stuff in CS).

url - Indicates a path within a given location. This works out such that a page name can be given as the path, but will always be directed to that page name underneath a folder (if one exists) for a named location. To that end, a url element contains attributes for a path, and a location. Each url also has a name. A url path may also contain string format token(s), and a related 'pattern' attributes. If a pattern exists, the url rewriter will format the path accordingly.

link - the config file itself shows what these are, but the thing to understand for our current problem is that we want to use a resourceUrl, not a navigateUrl which should be reserved for external URLs only. A resourceUrl points to a named url element, and itself has a unique name by which it will be referenced in skins and master files (more on that later). The other attributes are documented well enough in the config doc.

To put it all together: First, figure out if one of the existing locations will be relevant for your link. If you have a page under the root of the site, "common" will do. If you have created a new sub-folder, you should create a location for that folder. Second, create a url element that corresponds to the proper page you want to link to within your chosen location. Finally, create the link element that references your resourceUrl (url element), and either provide a 'text' attribute, or point to a named resource string (resource strings are in the resources.xml file in the proper language sub-folder under languages) to indicate how the link should be labelled.

Once this has been done, your link will be added to the tab-strip in the title bar. In order for the correct tab to be highlighted when you are on your page, you need to include a "SelectedNavigation" control on the master or skin for that page with the 'Selected' attribute value set equal to the name of the relevant link element. If one page will serve more than one title-bar link, you can programmatically adjust the "Selected" property of the global SelectedNavigation control in the Page_Init event:

void Page_Init(object sender, EventArgs e)
{
if(Request.QueryString["GroupID"] != null && Request.QueryString["GroupID"] == "8")
{
Context.Items["SelectedNavigation"] = "MyNewLink";
}
}
Submit this story to DotNetKicks

Thursday, June 08, 2006

Mini-update: Daemon on x64

Daemon Tools is now available for the 64-bit platform.

http://daemon.alphabravo.org/daemon403-x64.exe
Submit this story to DotNetKicks

Tuesday, June 06, 2006

Online virus-scan support for x64

I am happy to discover that the beta version of safety.live.com's Safety Center works wonderfully on my x64 machine. I have had a difficult time finding reliable anti-virus solutions for x64. All other online versions have failed at one level or another.

If you haven't checked out the tool, check it out here.
Submit this story to DotNetKicks

Wednesday, May 31, 2006

Update: Custom Membership Provider

Yep, it was just that easy. It took me a total of 2 hours to write and test a custom provider that would solve the hack problem I wrote about last time. It would take longer if you wanted to implement a full provider, but I only needed CreateUser and DeleteUser for this issue.

The documentation and examples for creating a membership provider are good, so there's no need to cover the basics. I did run into a couple of gotcha's.

First, I really didn't need total customization so I simply extended the Framework's SqlMembershipProvider. If you are using SqlServer, this works fine. The strange thing here is that the connection string, _sqlConnectionString in the provider, is private. This being the case I had to override Initialize and get the value for myself before passing the config along to the base class. Not difficult, just a silly repeat of existing code in the base.

My only other issue was with providing a good message to users if my custom provider Create or Delete operations gave an exception. The basic model on Create is to trap significant issues and log the specific problem while returning null as the MembershipUser and setting the
MembershipCreateStatus to some error type. In this case, the only applicable status is 'ProviderError' which causes a generic message to be displayed to the user. Logging isn't really helpful in this case, so I wrote the real message to a Session variable on the current HttpContext. OnCreateError, i set the error message to this session value.

Otherwise, all I really did was move my code from the OnUserCreated method into the Create method of my new provider. Wired this provider up instead of the other one, and viola!

Hats off to MS on this strategy implementation.
Submit this story to DotNetKicks

Tuesday, May 30, 2006

Membership Forms Auth Integration with Community Server

I recently set out to solve the following problem:

I have a community site running CommunityServer as one application running under a sub-domain url and acting as part of the site (in a different app) running on the base domain (and www sub-domain). The community site is running Community Server 2.0 and is setup to use forms authentication for Membership, along with Roles, and Profiles. CS uses the new provider model for asp.net 2.0, utilizing custom CS providers. The primary site is also asp.net 2.0 and is using the built-in .net framework sql providers. I want to be able to login or create a user in either site, and have that login and profile carry over to the other site.

The basic information you need to make this happen in .net forms authentication is straightforward, and is covered here. Make those changes first, then there are just a few adjustments to make to the base site and two tweaks to Community Server to complete the fix.

First, Community Server. The CS providers are configured in the web.config of the CS site. In order to have CS draw from the same Membership data as your base site they must share the application name attribute. You can change this in the web.config section for each provider. However, this isn't likely to change anything in CS. You also need to go into the CS database and change the ApplicationName column value in the cs_SiteSettings table. I couldn't find this as a configurable setting anywhere else in CS. The only other change to make to CS is to set the cookie domain to the same domain as the other site. Obviously this only works if our problem space is dealing in different sub-domains. To do this in the CS control panel go to AdministrationMembershipCookie and Anonymous Settings area and set the Cookie Domain value.

Once you have made these changes to CS, you'll need to restart the application pool.

At this point the two sites should allow login's from one site to carry over to the other (if you have cookies from earlier attempts, you'll need to delete them). The only problem is that if you allow new logins to be created at both sites, you'll need to align new member creation on the base site with member creation on the CS site. One way to deal with this issue is to direct member creation over to your CS site. I wanted my primary site to be self-contained so I decided to create CS-capable logins in the primary site.

First, the CS profile configuration has numerous attributes defined. You should add these to your profile configuration section of your main site's web.config. Second, CS has its own user and profile tables in the database that store additional community data for the standard Membership user created by the provider. The "right" way to do this may well be to create a custom provider. Like any good developer I have a lazy streak that set me looking for a quicker way to accomplish this. I did try to just use the CS providers in my main site. Unfortunately, these providers depend on way too many configuration settings and file locations based on the CS site for this to be worthwhile. What I settled on for now is to add the additional rows to the database in a handler for the CreatedUser event of my CreateUserWizard control. This does require a hack that is probably better dealt with by creating a custom provider, but I'll do that when I have more time.

For now there are two CS tables: cs_Users and cs_UserProfile which need new rows based on the created Membership user. Most of the columns have default values, but you'll need to supply the Membership based ID to both tables, and the new CS based UserId to the UserProfile table (FK), along with the proper SettingsID. Along with this, you need to add this new user to the proper roles associated with CS. By default new users are in the 'Everyone' and 'Registered Users' roles. A simple call to Roles.AddUserToRoles will take care of that.

The hack centers around what to do if the CS database updates fail. The problem is that the Membership operation already completed succesfully (which it must for you to have the FK data for cs_Users) and you are in an event that can no longer cancel the operation. Thus, you need to deal with rolling back the new user yourself and editing the CreateUserWizard messages and diplay format which the website user sees. In theory, a custom membership provider would have access to the create operations in time to deal with providing an error to the control. In this case, I just run the updates in a transaction and delete the new membership user if there is a failure during the cs table inserts. Finally, you have to set custom error text, and eliminate the success message from the control. Good enough to get going. I'll write more when I create the custom provider.

PS: A good article on how the dynamic profile properties are provided to intellisense is here.
Submit this story to DotNetKicks