Adding XFN to hCards in ASP.Net

10/13/2008

I said on Friday that I'd show you how to add XFN markup to an hCard.  First off though I should probably say what XFN is... XFN stands for XHTML Friend Network. It's basically a way to show relationships (think MySpace, etc.) by simply using hyperlinks.

In order to show these relationships we simply add a rel attribute to a hyperlink and use a set of pre defined relationship types (friend, met, crush, etc.). It's rather basic, doesn't take much, and doesn't cause any compatibility issues (in most instances anyway). For example, you can use it with hCards since hCards use the class attribute and XFN uses the rel attribute. While this might not seem all that useful, we can use these two items to create a nice directory or a contacts list. These items could then be crawled by outside sources (although I don't know of any hCard/XFN crawler at the moment). Anyway, to help you out and show you how easy it is to add to your code, I modified the vCard code further to add in XFN markup.

   1: /*
   2: Copyright (c) 2010 <a href="http://www.gutgames.com">James Craig</a>
   3: 
   4: Permission is hereby granted, free of charge, to any person obtaining a copy
   5: of this software and associated documentation files (the "Software"), to deal
   6: in the Software without restriction, including without limitation the rights
   7: to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
   8: copies of the Software, and to permit persons to whom the Software is
   9: furnished to do so, subject to the following conditions:
  10: 
  11: The above copyright notice and this permission notice shall be included in
  12: all copies or substantial portions of the Software.
  13: 
  14: THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15: IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16: FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17: AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18: LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19: OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  20: THE SOFTWARE.*/
  21:  
  22: #region Usings
  23: using System.Collections.Generic;
  24: using System.Text;
  25: using System.Text.RegularExpressions;
  26: #endregion
  27:  
  28: namespace Utilities.FileFormats
  29: {
  30:     /// <summary>
  31:     /// Class for creating vCards
  32:     /// </summary>
  33:     public class VCard
  34:     {
  35:         #region Constructor
  36:  
  37:         /// <summary>
  38:         /// Constructor
  39:         /// </summary>
  40:         public VCard()
  41:         {
  42:             Relationships = new List<Relationship>();
  43:         }
  44:  
  45:         #endregion
  46:  
  47:         #region Public Functions
  48:  
  49:         /// <summary>
  50:         /// Gets the vCard
  51:         /// </summary>
  52:         /// <returns>A vCard in string format</returns>
  53:         public string GetVCard()
  54:         {
  55:             StringBuilder Builder = new StringBuilder();
  56:             Builder.Append("BEGIN:VCARD\r\nVERSION:2.1\r\n");
  57:             Builder.Append("FN:");
  58:             if (!string.IsNullOrEmpty(Prefix))
  59:             {
  60:                 Builder.Append(Prefix).Append(" ");
  61:             }
  62:             Builder.Append(FirstName + " ");
  63:             if (!string.IsNullOrEmpty(MiddleName))
  64:             {
  65:                 Builder.Append(MiddleName).Append(" ");
  66:             }
  67:             Builder.Append(LastName);
  68:             if (!string.IsNullOrEmpty(Suffix))
  69:             {
  70:                 Builder.Append(" ").Append(Suffix);
  71:             }
  72:             Builder.Append("\r\n");
  73:  
  74:             Builder.Append("N:");
  75:             Builder.Append(LastName).Append(";").Append(FirstName).Append(";")
  76:                 .Append(MiddleName).Append(";").Append(Prefix).Append(";")
  77:                 .Append(Suffix).Append("\r\n");
  78:             Builder.Append("TEL;WORK:").Append(DirectDial).Append("\r\n");
  79:             Builder.Append("EMAIL;INTERNET:").Append(StripHTML(Email)).Append("\r\n");
  80:             Builder.Append("TITLE:").Append(Title).Append("\r\n");
  81:             Builder.Append("ORG:").Append(Organization).Append("\r\n");
  82:             Builder.Append("END:VCARD\r\n");
  83:             return Builder.ToString();
  84:         }
  85:  
  86:         /// <summary>
  87:         /// Gets the hCard version of the vCard
  88:         /// </summary>
  89:         /// <returns>A hCard in string format</returns>
  90:         public string GetHCard()
  91:         {
  92:             StringBuilder Builder = new StringBuilder();
  93:             Builder.Append("<div class=\"vcard\">");
  94:             if (string.IsNullOrEmpty(Url))
  95:             {
  96:                 Builder.Append("<div class=\"fn\">");
  97:                 if (!string.IsNullOrEmpty(Prefix))
  98:                 {
  99:                     Builder.Append(Prefix).Append(" ");
 100:                 }
 101:                 Builder.Append(FirstName).Append(" ");
 102:                 if (!string.IsNullOrEmpty(MiddleName))
 103:                 {
 104:                     Builder.Append(MiddleName).Append(" ");
 105:                 }
 106:                 Builder.Append(LastName);
 107:                 if (!string.IsNullOrEmpty(Suffix))
 108:                 {
 109:                     Builder.Append(" ").Append(Suffix);
 110:                 }
 111:                 Builder.Append("</div>");
 112:             }
 113:             else
 114:             {
 115:                 Builder.Append("<a class=\"fn url\" href=\"").Append(Url).Append("\"");
 116:                 if (Relationships.Count > 0)
 117:                 {
 118:                     Builder.Append(" rel=\"");
 119:                     foreach (Relationship Relationship in Relationships)
 120:                     {
 121:                         Builder.Append(Relationship.ToString()).Append(" ");
 122:                     }
 123:                     Builder.Append("\"");
 124:                 }
 125:                 Builder.Append(">");
 126:                 if (!string.IsNullOrEmpty(Prefix))
 127:                 {
 128:                     Builder.Append(Prefix).Append(" ");
 129:                 }
 130:                 Builder.Append(FirstName).Append(" ");
 131:                 if (!string.IsNullOrEmpty(MiddleName))
 132:                 {
 133:                     Builder.Append(MiddleName).Append(" ");
 134:                 }
 135:                 Builder.Append(LastName);
 136:                 if (!string.IsNullOrEmpty(Suffix))
 137:                 {
 138:                     Builder.Append(" ").Append(Suffix);
 139:                 }
 140:                 Builder.Append("</a>");
 141:             }
 142:             Builder.Append("<span class=\"n\" style=\"display:none;\"><span class=\"family-name\">").Append(LastName).Append("</span><span class=\"given-name\">").Append(FirstName).Append("</span></span>");
 143:             if (!string.IsNullOrEmpty(DirectDial))
 144:             {
 145:                 Builder.Append("<div class=\"tel\"><span class=\"type\">Work</span> ").Append(DirectDial).Append("</div>");
 146:             }
 147:             if (!string.IsNullOrEmpty(Email))
 148:             {
 149:                 Builder.Append("<div>Email: <a class=\"email\" href=\"mailto:").Append(StripHTML(Email)).Append("\">").Append(StripHTML(Email)).Append("</a></div>");
 150:             }
 151:             if (!string.IsNullOrEmpty(Organization))
 152:             {
 153:                 Builder.Append("<div>Organization: <span class=\"org\">").Append(Organization).Append("</span></div>");
 154:             }
 155:             if (!string.IsNullOrEmpty(Title))
 156:             {
 157:                 Builder.Append("<div>Title: <span class=\"title\">").Append(Title).Append("</span></div>");
 158:             }
 159:             Builder.Append("</div>");
 160:             return Builder.ToString();
 161:         }
 162:         #endregion
 163:  
 164:         #region Private Functions
 165:  
 166:         private static string StripHTML(string HTML)
 167:         {
 168:             if (string.IsNullOrEmpty(HTML))
 169:                 return string.Empty;
 170:  
 171:             HTML = STRIP_HTML_REGEX.Replace(HTML, string.Empty);
 172:             HTML = HTML.Replace("&nbsp;", " ");
 173:             return HTML.Replace("&#160;", string.Empty);
 174:         }
 175:  
 176:         private static readonly Regex STRIP_HTML_REGEX = new Regex("<[^>]*>", RegexOptions.Compiled);
 177:  
 178:         #endregion
 179:  
 180:         #region Properties
 181:         /// <summary>
 182:         /// First name
 183:         /// </summary>
 184:         public string FirstName { get; set; }
 185:  
 186:         /// <summary>
 187:         /// Last name
 188:         /// </summary>
 189:         public string LastName { get; set; }
 190:  
 191:         /// <summary>
 192:         /// Middle name
 193:         /// </summary>
 194:         public string MiddleName { get; set; }
 195:  
 196:         /// <summary>
 197:         /// Prefix
 198:         /// </summary>
 199:         public string Prefix { get; set; }
 200:  
 201:         /// <summary>
 202:         /// Suffix
 203:         /// </summary>
 204:         public string Suffix { get; set; }
 205:  
 206:         /// <summary>
 207:         /// Work phone number of the individual
 208:         /// </summary>
 209:         public string DirectDial { get; set; }
 210:  
 211:         /// <summary>
 212:         /// Email of the individual
 213:         /// </summary>
 214:         public string Email { get; set; }
 215:  
 216:         /// <summary>
 217:         /// Title of the person
 218:         /// </summary>
 219:         public string Title { get; set; }
 220:  
 221:         /// <summary>
 222:         /// Organization the person belongs to
 223:         /// </summary>
 224:         public string Organization { get; set; }
 225:  
 226:         /// <summary>
 227:         /// Relationship to the person (uses XFN)
 228:         /// </summary>
 229:         public List<Relationship> Relationships { get; set; }
 230:  
 231:         /// <summary>
 232:         /// Url to the person's site
 233:         /// </summary>
 234:         public string Url { get; set; }
 235:  
 236:         #endregion
 237:     }
 238:  
 239:     #region Enums
 240:     /// <summary>
 241:     /// Enum defining relationships (used for XFN markup)
 242:     /// </summary>
 243:     public enum Relationship
 244:     {
 245:         Friend,
 246:         Acquaintance,
 247:         Contact,
 248:         Met,
 249:         CoWorker,
 250:         Colleague,
 251:         CoResident,
 252:         Neighbor,
 253:         Child,
 254:         Parent,
 255:         Sibling,
 256:         Spouse,
 257:         Kin,
 258:         Muse,
 259:         Crush,
 260:         Date,
 261:         Sweetheart,
 262:         Me
 263:     }
 264:     #endregion
 265: }

With this, there are two added fields an URL and also a list of relationships (which is an enum type in the file, and also we can have more than one relationship specified). The enum list of relationships are the ones possible for XFN. However if you set the URL and add at least one relationship, the person's name will become a link pointing to the web site you specify with the appropriate relationships. And once we add it, we can format each relationship type using CSS. For example:

   1: a[rel~="crush"] { font-weight:bold;}

That would take all links marked as crush and bold them. Seems simple enough, but there are a couple down sides:

  1. No real use for XFN at present. Since there isn't a decently used crawler out there matching up people based on XFN information there really isn't much use for the information other than to tell the net that you're a coworker with Billy Bob (or Jimmy, or whomever since I don't know who you work with). Sites like Twitter and LinkedIn use it for friends lists and some sites can use it to import friends lists, but it's still not used to its fullest potential.
  2. It doesn't cover all instances. I like reading stuff that Scott Guthrie posts on his blog, so I have him on my blog roll. However I've never exactly met the guy, I'm not friends with him, etc. So how do I classify him? OK, thats not the best example as he'd fall under colleague but what if he were a botanist who posted pictures that I liked or something? There are certain situations where none of the tags work (they should add a cool tag or something, I don't know).
  3. No one coming to your site is going to care. To be honest, no one is going to take the time to find out that the bolded crush link means you have a crush on that person. Not a single one of them. You could create a legend but people still wont care that much... Maybe I'm wrong but I don't think I've ever bothered to check (on the sites that were using XFN) what the relationship to the person was...

So basically to sum it all up, it has potential. If you use it in a personalized contacts list or something along those lines (or on a company intranet, with some crawling tech put in place to keep track of the information), it could be useful. So download the code, try it out, leave feedback, and happy coding.



Comments