im in ur web, enriching ur code

 
 

February 2008 Entries

Drinking from the, erm... garden hose?

So, I finally signed up to dotnetkicks, and submitted my first story - it just happened to be my own "Build Providers: Strongly typed page urls in ASP.NET" post - which is an interesting take on improving productivity by using a build provider to create a strongly typed hierarchy of the files in your Visual Studio project, to avoid mis-typing page locations and improving intellisense.

On the same page

It turns out that Kirill Chilingarashvili was trying to solve the same issue with his solution last month - and while our methods in getting there differ, the end result is surprisingly similar. I only became aware of his method after submitting my post to dotnetkicks - I saw it a few entries down from mine just after I submitted it, and was a little jealous of his title "Auto generate strong typed navigation class for all user controls in ASP.NET web application" - how did I miss those very key words: auto generate and navigation class :)

It turns out that others thought the idea was interesting as well - and I feel the smallest amount of pride at the interested traffic dotnetkicks has sent my way. It's not exactly a deluge, like when you hear about sites getting slashdotted into the fiery pits of hell. No, but the near 500 views in a few days has totally outclassed my other posts which have been sitting there for some time, content rich of course, and earning visits from google.

Lets look at a few of my post stats...

Post Published Views Notes
The Smield: An unobtrusive javascript UI Helping Smart Field May 2007 652 Written 9 months ago, lots of content, indexed in google, most popular "all time" post... (but not for long?)
Building an Age Class in C# May 2007 552 Posted 9 months ago, not a lot of contention in search engines, gained a lot of popularity just before Christmas :)
Build Providers: Strongly typed page urls in ASP.NET Feb 2008 490 Published a few days ago, interesting content, submitted to community site (dotnetkicks).
Programmatically setting the SmtpClient pickup directory location at runtime Dec 2007 157 Recent addition, targeted content, good hit rate from search engines - pretty good popularity rise considering no outward push to gain popularity.

It's paltry figures by other popular bloggers standards (Haacked has almost 8,000 subscribers, egads!), but you have to start somewhere right?! It's about this time that you realise the power the community has, and how important it is if you want your 'hard work' and sharing to be seen and, erm, shared.

Originally, I wasn't going to 'push' or 'promote' articles out there. I thought that, if the content was good enough, readers would come to you (queue waynes world, if you build it, they will come, quote). It's still true that if you produce good content, you'll slowly gain readers and popularity. Search engines will slowly help you get there; Seeing that the build provider article posted to dotnetkicks has ousted my 'slow and steady' approach of my previous entries within the course of  couple of days, slow and steady seems a little too slow all of a sudden.

Initially, the thought of submitting a story I had also published seemed absurd, a little too much like 'gaming the system', but upon closer inspection, I saw a few bloggers I read regularly, posting their own content as well - some quite often.

Is this wrong?

From my perspective I want more people to read what I have to offer as I'm trying to fill a gap where I saw no previous content. The community is a great sounding board, it gives the ideas you have validation; how do you know if it's good, bad, works or not or is well-written if no one reads it? Google's certainly not going to tell me, "nice article Dave". It's a robot! At least not yet... I think there was a badly re-constituted analogy about trees and sound, and falling in the woods in there somewhere... but I'm not sure. I'm not regurgitating other peoples work, like what was going down for a brief period on the main feed of weblogs.asp.net after they opened up the blogs, but I wouldn't mind trying to get some of my older posts validated as it may be useful to others - or not. How will we know?

So, when does submitting your own stories to community sites become obnoxious and frowned upon? Technically, the submission is only the first minor step; the content itself draws the votes or 'kicks' and propels it up the list. Validation by popularity certainly sounds above board. Is there etiquette around such things?

It's not a community without you... and you.... and yeah, you too

I'm not sure why it took me so long to join dotnetkicks, but I'm glad I'm there now. If you're a .NET developer, I'd encourage you to sign up as well by shouting you a prized chocolate fish from my youth, but you won't need it once you realise what a great resource you've signed up to. And remember to kick things when you like them!

 

kick it on DotNetKicks.com

posted @ Wednesday, February 06, 2008 7:09 PM | Feedback (0)
Filed Under [ Off Topic, Community ]

Build Providers: Strongly typed page urls in ASP.NET

I'm a fan of strong typing in .NET as you gain compile time validation of code, Visual Studio Intellisense support, not to mention less mucking around with type conversion or dealing with (usually inconsistently) entering strings for page names and configuration setting keys inline. Visual Studio provides intellisense and some degree of validation for pages (i.e. urls) within a project when using Design Mode, but using those urls in code, e.g..

Response.Redirect( "~/MyDir/MyPage.aspx" );

you're out of luck as there isn't any validation unless you write it yourself.

Perhaps a better syntax is to access urls within your project from a property somewhere, like so:

Response.Redirect( MyDir_MyPage );

This way, we reference a static variable or constant to retrieve the url we want to redirect the browser to. If the variable is spelt wrong, the project won't compile and we can fix the error. Being a member within the project means we're provided with intellisense support as well.

However, maintaining variables or constants for your pages can be tedious. If you change the name of a page from "MyPage.aspx" to "MyMovedPage.aspx" the actual value of the variable  "MyDir_MyPage" would also need to be updated. Once again, unless you have some automated tests to validate the page exists, the project will compile, but the if you forgot to update the reference, you'll get the wrong url.

Using a Build Provider to deal with changing file names

To get around these limitations, and to help lessen the maintenance within your project, we can use a build provider to generate a static class containing all the page's in our project. Once that's done, we get:

  • Intellisense - no more typing page address strings in code.
  • Compile time support - if a page is renamed, the build provider updates the generated class, and changes the name of the variable. Since the variable you referenced in your project no longer exists, you'll get compile errors wherever the variable is used. It's a simple hop, skip and a jump to go through and correct those errors.

So now we'll be able to use the following code, and know that if we change something, we'll get notified when when we build if something is amis.

Response.Redirect( Href.MyDir.MyPage );

The code for the build provider generates the following psudo code:

  • create a static root class called "Href"
  • all pages within the root of the project will be exposed as string constants
  • recurse subfolders, adding nested/inner classes and members to represent the hierarchy of the project

We'll look at the code for the build provider a little later, first lets look at what it will generate.

It's all Constants and Nested Classes

Using the previous example, the code the build provider generates looks something along the lines of...

public static class Href

{

  // ... repeat, one constant for each

  // file in the project root folder

  public const string Default = "~/";

 

  // ... repeat, one "nested class" for

  // each folder in the project root folder

  public static class MyDir

  {

    // ... repeat, one constant for each

    // file in the folder: /MyDir

    public const string MyPage = "~/mydir/mypage.aspx";

  }

}

Next: Setting up the build provider and configuration options.

Build providers must be compiled and referenced in the /bin folder of your project. You can't simply add the code for the build provider to the App_Code folder, as it will give a "Could not load type ''MyBuildProvider" error.

Once we've added the assembly reference to our project, we can add the relevant configuration information to the web.config.

<system.web>

  <compilation>

    <buildProviders>

      <add extension=".hrefs" type="CSharpVitamins.Compilation.HrefBuildProvider, CSharpVitamins"/>

    </buildProviders>

  </compilation>

</system.web>

Here I've chosen to associate files with the ".hrefs" extension with my HrefBuildProvider. You could easily choose another extension, as it's simply a mapping for a file extension found within the App_Code folder and the build provider. 

Now we can create a file with that extension in the App_Code folder (let's use "Site.hrefs" for now, but you could use any name. You could even have multiple .hrefs files with different class names, one for aspx pages, another for your images, javascript and css files :), and add the following configuration - then tailor to suit your preferences.

<?xml version="1.0" encoding="utf-8" ?>

<settings

  namespace=""

  className="Href"

  maxDepth="100"

  lowercaseUrls="true"

  includeExtension="false"

  makePartial="false">

 

  <files include="\.(as[pcmh]x|html?)$" exclude="" />

  <folders include="" exclude="App_|Bin" />

</settings>


Setting Description
namespace The namespace the generated class will be placed within. Leave blank to add to the global namespace. 
className The actual class name that forms the stub of all urls. Defaults to "Href".
maxDepth The maximum depth of recursion for processing. Enter a value of 1 for root files only, or higher to include subfolders. Defaults to 100.
lowercaseUrls When true, forces the values of urls to lowercase. Handy of you want to have to pages named in PascalCase but want urls all lowercase. Property names follow the same casing as the file name. Defaults to false.
includeExtension When true, the extension is appended to the property name separated by an underscore. Defaults to false.
makePartial When true, the root class uses the partial modifier, enabling you to add additional members through a non-auto-generated class file.
files Contains two regular expression patterns, once each for files to include and exclude. Include defaults to "\.aspx$", Exclude defaults to empty (none are explicitly excluded).
folders Same as files, but applied to folders. Include default to empty (all are included), Exclude defaults to "App_|Bin".

Apart from the juicy code, which is coming next, that's it. Now you'll have intellisense for your file locations and compile time checking. It's worth noting that the generated class from the build provider will regenerate when the .hrefs file is modified, or you rebuild. If you want to see the generated class (it's stored in the "Temporary ASP.NET Files" folder), I'd suggest simply to right click on "Href" from your code, and choose, "Go to definition" - it will bring up the temporary file for you to peruse.

Source code for the HrefBuildProvider

Rather than walking through the step by step of creating a build provider, I'll just point you to a couple of other articles on the subject if you wish to learn more, then I can just display the fully commented source code for the class below :)

You can download the source code for the HrefBuildProvider or look at the class definition below.

using System;

using System.CodeDom;

using System.Collections.Generic;

using System.IO;

using System.Text.RegularExpressions;

using System.Web.Compilation;

using System.Web.Hosting;

using System.Xml;

 

namespace CSharpVitamins.Compilation

{

/// <summary>

/// Generates a static class for urls within an ASP.NET project

/// </summary>

public class HrefBuildProvider : BuildProvider

{

  #region Fields

 

  /// <summary>

  /// Path to the web root for determining urls within the site

  /// </summary>

  string _basePath;

  /// <summary>

  /// Whether to convert urls to lowercase - based on your preference

  /// </summary>

  bool _useLowerCaseUrls;

  /// <summary>

  /// Whether to include the extension of the file in the name

  /// of the member

  /// </summary>

  bool _includeExtension;

  /// <summary>

  /// Whether to create the root class as 'partial', allowing

  /// additional members to be added to the resultant class.

  /// </summary>

  bool _makePartial;

  /// <summary>

  /// Maximum recurring depth for the project - 1 for top level,

  /// 2 for first level of subfolders etc...

  /// </summary>

  int _maxDepth = 100;

  /// <summary>

  /// Namespace the generated class will be placed within.

  /// Leave empty to add to the global namespace e.g. global::Href

  /// </summary>

  string _namespace;

  /// <summary>

  /// The name of the main class e.g. Href

  /// </summary>

  string _className;

 

  /// <summary>

  /// Current depth of recursion

  /// </summary>

  int _depth = 0;

  /// <summary>

  /// Predicate to determine if a file should be included

  /// </summary>

  Predicate<FileSystemInfo> isValidFile;

  /// <summary>

  /// Predicate to determine if a folder should be included

  /// </summary>

  Predicate<FileSystemInfo> isValidFolder;

 

  #endregion

 

  #region GenerateCode

 

  /// <summary>

  /// Initialises settings from the BuildProvider file, and

  /// generates the appropriate code.

  /// </summary>

  /// <param name="assemblyBuilder"></param>

  public override void GenerateCode(AssemblyBuilder assemblyBuilder)

  {

    // init settings from xml source file

    init();

 

    // create root class e.g. global::Href

    CodeTypeDeclaration root = createStaticClass(_className, _makePartial);

    addSummary(root, "Provides access to urls within the project.");

    build(new DirectoryInfo(_basePath), root);

 

    CodeNamespace ns = new CodeNamespace();

    ns.Types.Add(root);

 

    // leave blank for global::namespace

    if (!string.IsNullOrEmpty(_namespace))

      ns.Name = _namespace;

 

    CodeCompileUnit unit = new CodeCompileUnit();

    unit.Namespaces.Add(ns);

    assemblyBuilder.AddCodeCompileUnit(this, unit);

  }

 

  #endregion

 

  #region build

 

  /// <summary>

  /// Iterates over the given directory, adding the files as members to

  /// the parent type, then recurse down the folder tree until the max

  /// depth is reached.

  /// </summary>

  /// <param name="dir">The directory to process</param>

  /// <param name="parent">Parent class to add members too.</param>

  void build(DirectoryInfo dir, CodeTypeDeclaration parent)

  {

    if (_depth >= _maxDepth)

      return;

 

    ++_depth;

 

    /// 'members' keeps a record of the number of times a

    /// member name is repeated within the parent type/class.

    /// Pass this to 'ensureUniqueMemberName' to get the name

    /// with an index number appended to the end e.g. the second

    /// occurrence of "MyProperty" becomes "MyProperty1"

    Dictionary<string, int> members = new Dictionary<string, int>();

 

    ///

    /// process files:

    /// iterate over files and add the member

    /// public const string MyfileName = "~/myfileName.aspx";

    ///

    FileInfo[] files = dir.GetFiles();

    foreach (FileInfo file in files)

    {

      if (isValidFile(file))

      {

        CodeMemberField field = new CodeMemberField();

        field.Name = getName(file);

        field.Type = new CodeTypeReference(typeof(string));

        field.Attributes = MemberAttributes.Public | MemberAttributes.Const;

        field.InitExpression = getInitExpression(getUrl(file));

        addSummary(field, getUrl(file, false));

        ensureUniqueMemberName(members, field);

 

        parent.Members.Add(field);

      }

    }

 

    ///

    /// process subfolders:

    /// iterate over folders and add a nested class

    ///

    DirectoryInfo[] subfolders = dir.GetDirectories();

    foreach (DirectoryInfo folder in subfolders)

    {

      if (isValidFolder(folder))

      {

        CodeTypeDeclaration nested = createStaticClass(getName(folder), false);

        addSummary(nested, "Provides access to urls under: {0}", getUrl(folder, false).TrimStart('~'));

        ensureUniqueMemberName(members, nested);

        build(folder, nested);

 

        /// .ctor will have already been added to members

        /// so only add to parent if there are additional

        /// members present

        if (nested.Members.Count > 1)

          parent.Members.Add(nested);

      }

    }

 

    --_depth;

  }

 

  #endregion

 

  #region init

 

  /// <summary>

  /// Initialises settings from the config file.

  /// </summary>

  /// <example>

  ///

  /// Sample configuration - all nodes and elements are optional, but the file

  /// needs a root node to load as an xml document.

  /// <?xml version="1.0" encoding="utf-8" ?>

  /// <settings

  ///    namespace=""

  ///    className="Href"

  ///    maxDepth="100"

  ///    lowercaseUrls="true"

  ///    includeExtension="false">

  ///   

  ///    <files include="\.(aspx|html)$" exclude="\.htm$" />

  ///    <folders include="" exclude="App_|Bin|Templates" />

  ///  </settings>

  ///

  /// </example>

  void init()

  {

    _basePath = HostingEnvironment.ApplicationPhysicalPath;

 

    XmlDocument xml = new XmlDocument();

    using (Stream stream = VirtualPathProvider.OpenFile(base.VirtualPath))

      xml.Load(stream);

 

    /// code below uses syntax,

    ///      xml["elementName"] ?? xml.CreateElement( "elementName" );

    /// as a lazy initialisation technique, rather than checking for

    /// the node and performing alternate initialisation - this keeps

    /// it within the same reading context.

 

    ///

    /// general settings

    ///

    XmlElement settings = xml["settings"] ?? xml.CreateElement("settings");

    _maxDepth = int.Parse(getAttributeValue(settings, "maxDepth", "100"));

    _useLowerCaseUrls = bool.Parse(getAttributeValue(settings, "lowercaseUrls", "false"));

    _includeExtension = bool.Parse(getAttributeValue(settings, "includeExtension", "false"));

    _makePartial = bool.Parse(getAttributeValue(settings, "makePartial", "false"));

    _namespace = getAttributeValue(settings, "namespace");

    _className = getAttributeValue(settings, "className", "Href");

 

    ///

    /// files settings

    ///

    XmlElement files = settings["files"] ?? xml.CreateElement("files");

    isValidFile = createFilter(

      getAttributeValue(files, "include", @"\.aspx$"),

      getAttributeValue(files, "exclude")

      );

 

    ///

    /// folders settings

    ///

    XmlElement folders = settings["folders"] ?? xml.CreateElement("folders");

    isValidFolder = createFilter(

      getAttributeValue(folders, "include"),

      getAttributeValue(folders, "exclude", "App_|Bin")

      );

  }

 

  #endregion

 

  #region getAttributeValue

 

  /// <summary>

  ///

  /// </summary>

  /// <param name="parent"></param>

  /// <param name="name"></param>

  /// <param name="default"></param>

  /// <returns></returns>

  static string getAttributeValue(XmlElement parent, string name, string @default)

  {

    XmlAttribute attribute = parent.Attributes[name];

    return null == attribute ? @default : attribute.Value.Trim();

  }

 

  /// <summary>

  ///

  /// </summary>

  /// <param name="parent"></param>

  /// <param name="name"></param>

  /// <returns></returns>

  static string getAttributeValue(XmlElement parent, string name)

  {

    return getAttributeValue(parent, name, null);

  }

 

  #endregion

 

  #region createFilter

 

  /// <summary>

  /// Creates a Predicate for filtering based on the given regex patterns

  /// </summary>

  /// <param name="includePattern"></param>

  /// <param name="excludePattern"></param>

  /// <returns></returns>

  static Predicate<FileSystemInfo> createFilter(string includePattern, string excludePattern)

  {

    Regex include = null, exclude = null;

 

    if (!string.IsNullOrEmpty(includePattern))

      include = new Regex(includePattern,

        RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    if (!string.IsNullOrEmpty(excludePattern))

      exclude = new Regex(excludePattern,

        RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    return createFilter(include, exclude);

  }

 

  /// <summary>

  /// Creates a Predicate for filtering based on the given regex

  /// </summary>

  /// <param name="include"></param>

  /// <param name="exclude"></param>

  /// <returns></returns>

  static Predicate<FileSystemInfo> createFilter(Regex include, Regex exclude)

  {

    if (null != include && null != exclude)

      return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName) && !exclude.IsMatch(info.FullName); };

    else if (null != include)

      return delegate(FileSystemInfo info) { return include.IsMatch(info.FullName); };

    else if (null != exclude)

      return delegate(FileSystemInfo info) { return !exclude.IsMatch(info.FullName); };

    else

      return delegate(FileSystemInfo info) { return true; };

  }

 

  #endregion

 

  #region getInitExpression

 

  /// <summary>

  /// Returns the given value surrounded with quotes.

  /// </summary>

  /// <param name="value"></param>

  /// <returns></returns>

  static CodeSnippetExpression getInitExpression(string value)

  {

    return new CodeSnippetExpression(string.Concat("\"", value, "\""));

  }

 

  #endregion

 

  #region getName

 

  /// <summary>

  /// Gets the normalised/escaped member name for the given file

  /// </summary>

  /// <param name="file"></param>

  /// <returns></returns>

  string getName(FileSystemInfo file)

  {

    string name = Path.GetFileNameWithoutExtension(file.Name);

 

    if (_includeExtension)

      name += file.Extension.Replace('.', '_');

 

    name = Regex.Replace(name, @"[^a-z0-9_]*", string.Empty,

      RegexOptions.Compiled | RegexOptions.IgnoreCase);

 

    // Ensure pascal casing of the name - not really required

    //name = TextHelper.PascalCase( name );

 

    // Escape names not starting with a letter

    if (name.Length > 0 && !char.IsLetter(name[0]))

      name = "_" + name;

 

    // Escape C# keywords

    // included in separate library, omitted here for clarity

    //name = CodeHelper.CSharp.EscapeWord(name);

 

    return name;

  }

 

  #endregion

 

  #region getUrl

 

  /// <summary>

  /// Gets the url of the file, relative to the app root

  /// </summary>

  /// <param name="file"></param>

  /// <returns></returns>

  string getUrl(FileSystemInfo file)

  {

    return getUrl(file, true);

  }

 

  /// <summary>

  /// Gets the url of the file, relative to the app root

  /// </summary>

  /// <param name="file"></param>

  /// <param name="allowNormalisation">When true, allows lowercasing of the

  /// url and stripping of default.aspx</param>

  /// <returns></returns>

  string getUrl(FileSystemInfo file, bool allowNormalisation)

  {

    string url = file.FullName.Substring(_basePath.Length);

 

    if (allowNormalisation)

    {

      if (_useLowerCaseUrls)

        url = url.ToLower();

 

      if (url.EndsWith("default.aspx", StringComparison.OrdinalIgnoreCase))

        url = url.Substring(0, url.Length - 12);

    }

 

    return string.Concat("~/", url.Replace("\\", "/"));

  }

 

  #endregion

 

  #region addSummary

 

  /// <summary>

  /// Adds a summary doc comment to the type's comment collection

  /// </summary>

  /// <param name="type"></param>

  /// <param name="format"></param>

  /// <param name="args"></param>

  void addSummary(CodeTypeMember type, string format, params object[] args)

  {

    addSummary(type.Comments, format, args);

  }

 

  /// <summary>

  /// Adds a summary doc comment to the collection

  /// </summary>

  /// <param name="comments"></param>

  /// <param name="format"></param>

  /// <param name="args"></param>

  void addSummary(CodeCommentStatementCollection comments, string format, params object[] args)

  {

    comments.Add(new CodeCommentStatement("<summary>", true));

    comments.Add(new CodeCommentStatement(string.Format(format, args), true));

    comments.Add(new CodeCommentStatement("</summary>", true));

  }

 

  #endregion

 

  #region createStaticClass

 

  /// <summary>

  /// Creates a static class type with the given name

  /// </summary>

  /// <param name="name"></param>

  /// <param name="partial"></param>

  /// <returns></returns>

  static CodeTypeDeclaration createStaticClass(string name, bool partial)

  {

    CodeTypeDeclaration type = new CodeTypeDeclaration(name);

    type.TypeAttributes |= System.Reflection.TypeAttributes.Sealed;

    type.Attributes = MemberAttributes.Public | MemberAttributes.Static;

    type.IsClass = true;

    type.IsPartial = partial;

 

    CodeConstructor ctor = new CodeConstructor();

    ctor.Attributes = MemberAttributes.Private;

    type.Members.Add(ctor);

 

    return type;

  }

 

  #endregion

 

  #region ensureUniqueMemberName

 

  /// <summary>

  /// Ensures a unique name within the dictionary to get the name

  /// with an index number appended to the end e.g. the second

  /// occurrence of "MyProperty" becomes "MyProperty1"

  /// </summary>

  /// <remarks>A caveat of this method is where you may have two members

  /// like MyPage.aspx, MyPage.html as well as MyPage1.aspx. A conflict

  /// will occur giving duplicate members i.e. in the same order MyPage,

  /// MyPage1 and MyPage1 a second time. The likelihood of such conflicts

  /// is possible, with one possible solution to recurse using

  /// ensureUniqueMemberName with the newly generated name, or enumerate

  /// the parent type's Members to see whether there will be a conflict.

  /// </remarks>

  /// <param name="members">Dictionary containing the name of the members

  /// in the current namespace as the key + the number of appearances as

  /// the value.</param>

  /// <param name="type">The next member to add to the current namespace.</param>

  static void ensureUniqueMemberName(Dictionary<string, int> members, CodeTypeMember type)

  {

    string key = type.Name;

    int count;

    if (members.TryGetValue(key, out count))

    {

      type.Name = string.Concat(key, ++count);

      members[key] = count;

    }

    else

    {

      members[key] = 0;

    }

  }

 

  #endregion

}

}

 

kick it on DotNetKicks.com

posted @ Sunday, February 03, 2008 3:51 PM | Feedback (12)
Filed Under [ C#, Utilities, Tips, ASP.NET, Source Code ]

Recently on C# Vitamins...

Powered By Subtext

 

About C# Vitamins

Dave has been working in the industry for around 14 years, and has a focus on Javascript, C#, ASP.NET and SQL Server web development; not to mention being a standards driven type of guy.

C# Vitamins is the result of his findings while working in the web industry and a desire to share with the community; and if it was traced back far enough, you might say it might not have existed if he hadn't taken such an interest in id Software's original Quake.

Related Links

Below is a list of related links of Dave's other sites.