Check out my new blog at

Monday, June 30, 2008

It’s all Relative

Last week I took a trip to visit friends. It has been a while since I last traveled (and I can say that is a first for about the last ten years). I flew through Memphis both ways. On the return trip, I had an interesting experience. I went to the airport-supplied-really-expensive-and-risky-to-eat food trough so thoughtfully allowed by the airport to hawk its wares, and struck up a conversation with a young man. Now, this wasn't just any young man. Standing behind him, I noticed he had dog-tags, had a tightly shaved head, and sported desert combat boots. These are not run-of-the-mill REI weekend warrior type boots and his were worn well. Otherwise, he was garbed in civilian clothes.

It was obvious he was in the service. I lived in Washington DC for about 4 years working for a program management company that did plenty of DOD work and learned a thing or two about the military. I am not an expert, but I know the average recruit joining the armed service is hardly swimming in money.

So, I tapped him on the shoulder and asked him if I could buy him lunch. Actually, I insisted. I am not a fan of the current war or any war, for that matter; but these young people deserve our attention—and our support. We may not agree with our government's policies in every case, but I think those wearing the uniform deserve absolute respect.

We stood there in line for just a few moments, talking about what he had been through. He had just spent six months in Africa protecting aide workers delivering food and medicine to the local population. As it turned out, this young man is a Marine. There is no way to understand the Marine viewpoint unless you are one, but I have friends and family that are or were in the Marine Corp, and I can tell you they are hard core; yet, this young man never fired a shot in anger—he simply enabled those in need to get the help they so desperately require. I asked him if he had been to Iraq and he said "no, nobody's been shot for me to replace." Wow.

Interestingly, this young man had a newly purchased book tucked under his arm that was a current events book (obviously written before Hillary conceded) contrasting Clinton against McCain. I had to ask…

"So, who are you voting for in this election?" I asked nonchalantly.

"McCain, because he will fund the [insert explicative] out of me and my guys…" He responded with obvious passion.

Well, what can you say to that? Things are mostly relative to one's own viewpoint. I am by no means a wealthy person but I am healthy, I have a great family, and I earn a good living. This young man is laying his life on the line every day because his country asked him to (let's not forget that the American military is entirely voluntary). He has to do it poor, scared, and dirty most of the time.

Buying him lunch was the best ten dollars I have spent in a long time.

Young Marine, if you are out there reading this, realize that there are many of us that don't support the war, but we will always be faithful to you—after all, that is what Semper Fi means.



Tuesday, June 24, 2008

Microsoft Live Mesh

My friend Brian (The Knowledge Base) Kennemer, invited me to participate in Microsoft's Tech Preview of Live Mesh.  Live Mesh allows you to connect your devices in a cloud, share folders across devices and with others, and stay informed about changes to data via a feed.   I am sharing mpFx code with a few colleagues by sharing my source directory out via Mesh.  Unlike a traditional network share, Live Mesh shared folders are peer-to-peer (+ cloud for management), thus allowing my colleagues to have an offline copy of the source code, which they can then synch back up to when they reconnect.  Ray Ozzie and team have been working hard on Mesh for a while and its Groove heritage is apparent but much improved upon.

I have been experimenting quite a bit with Mesh.  I shared out my Microsoft Project local cache to a couple machines so I can take a project offline to work on it on any device.  As long as I am careful not to open Project on different devices simultaneously, this works pretty well (BUT DON'T DO THIS WITH PRODUCTION DATA).  My little problem with sharing my OneNote notebooks is handily solved, thank you very much.

I am off to visit the Mesh team's wish list.  I have a bunch of single-user applications that I would like to restrict access to shared data across multiple machines.  The applications essentially need to be singletons across multiple devices in order for the shared data to not get corrupted.  It would be neat if I could wrap the startup of one of these applications in a shortcut that would be responsible for asking the Mesh cloud for a lock-token for the application.  If I then tried to start the application on another device (also through a specialized shortcut), Mesh would prevent me from doing so in order to maintain data integrity.  Right click on executable and "Run as Mesh Shared Application"...

Tuesday, June 17, 2008

YALT: Adding Headers to C# Source Files with Subversion Integration & Auto Fields

That's "Yet Another Little Tool"... In an endless series of diversions to keep myself somewhat challenged, last night I wrote a little tool that applies a header to C# source files. 

See the screen shot below. 


The header text box is directly editable and recognizes tokens opened by "[" and closed by "]".  When detected, a row is added (or removed if an existing field is removed from the header) to the Replaceable Fields grid view.  The grid view displays an image in the first column coupled the the feedback column (which in turn indicates whether the field is supported, properly constructed and a value provided for if not an auto field or a Subversion field--we will talk about this in a minute).  The second column is the name of the replaceable field, the third column is the value to replace the field name with in the header text (if the field is auto field or a Subversion field, the value is not supplied manually).

So, what does this mean?  I can create (and save and load, incidentally) header templates which will automatically be appended to any C# source file.  Furthermore, if a Subversion URL is provided that matches the source directory, the tool will query Subversion and automatically populate the SVN:AUTHOR field and the SVN:CREATED_DATE field and optionally append Subversion log messages to the end of the source file as comments.

Also, the tool supports a limited vocabulary of AUTO:* fields, such as the FILE (file name) auto field.  Auto fields are, as you might expect, automatically filled in for you.

The rest of the fields such as COMPANY NAME, PRODUCT NAME, and COPYRIGHT use name/value pairs supplied by the user.  See the screen shot below.  Any number of fields may be added to the header.


Probably the most interesting thing about this little tool is the use of SharpSVN to query Subversion for information about the source files.  Either I am missing something, or the documentation is really quite lacking for SharpSVN.  I was able to figure out how to do the following:

  1. Connect to an Subversion repository
  2. Get the revision list for a particular file
  3. Get additional information about a file for the first revision (in order to retrieve the AUTHOR and CREATED_DATE fields)
  4. Get the list of log messages supplied during check in (as I mentioned, these are optionally appended to the end of the source file as comments).

Here is the code snippet:

   1: string rootPath = Path.GetFileName(_SvnTarget);
   3: SvnClient svnClient = new SvnClient();
   5: SvnTarget svnTarget;
   7: SvnInfoArgs svnInfoArgs = new SvnInfoArgs();
   9: string svnPath = _SvnTarget + sourceFile.Substring(sourceFile.IndexOf(@"\" + rootPath) + (@"\" + rootPath).Length);
  11: svnPath = svnPath.Replace(@"\", "/");
  13: if (SvnTarget.TryParse(svnPath, out svnTarget))
  14: {
  16:     // Get the revision list
  17:     Collection<SvnLogEventArgs> outSvnLogEventArgs;
  18:     svnClient.GetLog(new Uri(svnTarget.ToString()), out outSvnLogEventArgs);
  20:     svnInfoArgs.Revision = outSvnLogEventArgs[0].Revision;
  22:     // Get the first revision
  23:     Collection<SvnInfoEventArgs> outSvnInfoEventArgs;
  24:     svnClient.GetInfo(svnTarget, svnInfoArgs, out outSvnInfoEventArgs);
  26:     // Save the author and created date
  27:     string author = outSvnInfoEventArgs[0].LastChangeAuthor;
  28:     string dateCreated = outSvnInfoEventArgs[0].LastChangeTime.ToLongDateString();
  30:     // Retrieve the log messages 
  31:     if (_AppendLogMessagesAsFooter)
  32:     {
  33:         StringBuilder comments = new StringBuilder();
  35:         foreach (SvnLogEventArgs logEventArg in outSvnLogEventArgs)
  36:         {
  37:             if (!string.IsNullOrEmpty(logEventArg.LogMessage))
  38:             {
  39:                 comments.Append("\n");
  40:                 comments.Append(@"//");
  41:                 comments.Append(logEventArg.Time.ToShortDateString());
  42:                 comments.Append(" : ");
  43:                 comments.Append(logEventArg.Author);
  44:                 comments.Append(" - ");
  45:                 comments.Append(logEventArg.LogMessage);
  46:             }
  47:         }
  49:         footer = comments.ToString();
  50:     }
  51: }
The files to apply the header to are selected via a source browser:

Monday, June 16, 2008

Microsoft Project FX (mpFx) - Project Server Groups

Below is an mpFx class that provides data and functionality for managing Project Server groups, plus a helper method for retrieving the Active Directory groups for the default AD root.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Diagnostics;
   4: using System.DirectoryServices;
   5: using CodePlex.MicrosoftProject.mpFx.SecurityWebService;
   7: namespace CodePlex.MicrosoftProject.mpFx
   8: {
   9:     public class Groups
  10:     {
  11:         private Dictionary<Guid, string> _Groups;
  13:         protected internal Groups(Security parent)
  14:         {
  15:             Parent = parent;
  16:         }
  18:         protected internal Security Parent { get; protected set; }
  20:         /// <summary>
  21:         /// Retrieve a dictionary of Group GUIDS and Names from Project Server
  22:         /// </summary>
  23:         public Dictionary<Guid, string> GetGroupsList(bool refresh)
  24:         {
  25:             if (_Groups == null || refresh)
  26:             {
  27:                 _Groups = new Dictionary<Guid, string>();
  29:                 using (SecurityGroupsDataSet securityGroupsDataSet = Parent.Parent.WebServices.Security.ReadGroupList())
  30:                 {
  31:                     foreach (SecurityGroupsDataSet.SecurityGroupsRow group in securityGroupsDataSet.SecurityGroups.Rows)
  32:                     {
  33:                         _Groups.Add(group.WSEC_GRP_UID, group.WSEC_GRP_NAME);
  34:                     }
  35:                 }
  36:             }
  38:             return _Groups;
  39:         }
  41:         #region Group Methods
  43:         /// <summary>
  44:         /// Create a new group
  45:         /// </summary>
  46:         /// <param name="groupGuid">The group's GUID</param>
  47:         /// <param name="name">The group's name</param>
  48:         /// <param name="description">A description of the group</param>
  49:         /// <param name="adGroupName">The group's Active Directory if relevant</param>
  50:         /// <param name="adGroupGuid">The group's Active Directory objectGUID if relevant</param>
  51:         public void CreateGroup(Guid groupGuid,
  52:                                 string name,
  53:                                 string description,
  54:                                 string adGroupName,
  55:                                 Guid adGroupGuid)
  56:         {
  57:             using (SecurityGroupsDataSet securityGroupsDataSet = new SecurityGroupsDataSet())
  58:             {
  59:                 SecurityGroupsDataSet.SecurityGroupsRow group = securityGroupsDataSet.SecurityGroups.NewSecurityGroupsRow();
  61:                 group.WSEC_GRP_UID = groupGuid;
  62:                 group.WSEC_GRP_NAME = name;
  63:                 group.WSEC_GRP_DESC = description;
  64:                 group.WSEC_GRP_AD_GROUP = adGroupName;
  66:                 if (adGroupGuid != Guid.Empty)
  67:                 {
  68:                     group.WSEC_GRP_AD_GUID = adGroupGuid;
  69:                 }
  71:                 securityGroupsDataSet.SecurityGroups.AddSecurityGroupsRow(group);
  73:                 Parent.Parent.WebServices.Security.CreateGroups(securityGroupsDataSet);
  74:             }
  75:         }
  77:         /// <summary>
  78:         /// Create a new group and add resources to it
  79:         /// </summary>
  80:         /// <param name="groupGuid">The group's GUID</param>
  81:         /// <param name="name">The group's name</param>
  82:         /// <param name="description">A description of the group</param>
  83:         /// <param name="adGroupName">The group's Active Directory if relevant</param>
  84:         /// <param name="adGroupGuid">The group's Active Directory objectGUID if relevant</param>
  85:         /// <param name="resourceGuids">An array of resource GUID for which memebership we be established</param>
  86:         public void CreateGroup(Guid groupGuid,
  87:                                 string name,
  88:                                 string description,
  89:                                 string adGroupName,
  90:                                 Guid adGroupGuid,
  91:                                 Guid[] resourceGuids)
  92:         {
  93:             CreateGroup(groupGuid, name, description, adGroupName, adGroupGuid);
  94:             AddResourcesToGroup(groupGuid, resourceGuids);
  95:         }
  97:         /// <summary>
  98:         /// Add users to a group
  99:         /// </summary>
 100:         /// <param name="groupGuid">The GUID of the group</param>
 101:         /// <param name="resourceGuids">An array of resource GUID for which memebership we be established</param>
 102:         public void AddResourcesToGroup(Guid groupGuid,
 103:                                         Guid[] resourceGuids)
 104:         {
 105:             if (groupGuid == Guid.Empty || !_Groups.ContainsKey(groupGuid))
 106:             {
 107:                 throw new ArgumentException("Invalid group");
 108:             }
 110:             using (SecurityGroupsDataSet securityGroupsDataSet = Parent.Parent.WebServices.Security.ReadGroup(groupGuid))
 111:             {
 112:                 foreach (Guid resourceGuid in resourceGuids)
 113:                 {
 114:                     if (securityGroupsDataSet.GroupMembers.FindByRES_UIDWSEC_GRP_UID(resourceGuid, groupGuid) == null)
 115:                     {
 116:                         SecurityGroupsDataSet.GroupMembersRow groupMembership = securityGroupsDataSet.GroupMembers.NewGroupMembersRow();
 118:                         groupMembership.RES_UID = resourceGuid;
 119:                         groupMembership.WSEC_GRP_UID = groupGuid;
 121:                         securityGroupsDataSet.GroupMembers.AddGroupMembersRow(groupMembership);
 122:                     }
 123:                 }
 125:                 Parent.Parent.WebServices.Security.SetGroups(securityGroupsDataSet);
 126:             }
 127:         }
 129:         /// <summary>
 130:         /// Retrieve dictionary of Active Directory groups for the current default search root
 131:         /// </summary>
 132:         /// <returns></returns>
 133:         public Dictionary<string, Guid> GetADGroups()
 134:         {
 135:             DirectorySearcher directorySearcher = new DirectorySearcher {Filter = "(&(objectCategory=group))", SearchScope = SearchScope.Subtree};
 137:             directorySearcher.PropertiesToLoad.Add("name");
 138:             directorySearcher.PropertiesToLoad.Add("objectGUID");
 140:             SearchResultCollection searchResultCollection = directorySearcher.FindAll();
 142:             Dictionary<string, Guid> results = new Dictionary<string, Guid>();
 144:             foreach (SearchResult result in searchResultCollection)
 145:             {
 146:                 Debug.Assert(result.Properties["name"].Count == 1 && result.Properties["objectGUID"].Count == 1);
 148:                 results.Add(result.Properties["name"][0].ToString(), new Guid((byte[]) result.Properties["objectGUID"][0]));
 149:             }
 151:             return results;
 152:         }
 154:         #endregion
 155:     }
 156: }


Content on this site is provided "AS IS" with no warranties and confers no rights. Additionally, all content on this site is my own personal opinion and does not represent my employer's view in any way.