<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Nathanael Jones &#187; ASP.NET</title>
	<atom:link href="http://nathanaeljones.com/tag/asp-net/feed/" rel="self" type="application/rss+xml" />
	<link>http://nathanaeljones.com</link>
	<description>Ramblings of a computer linguist</description>
	<lastBuildDate>Mon, 04 Jan 2010 21:12:43 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Extending Page: Adding Metadata and Stylesheet management</title>
		<link>http://nathanaeljones.com/152/extending-page-adding-metadata-and-stylesheet-management/</link>
		<comments>http://nathanaeljones.com/152/extending-page-adding-metadata-and-stylesheet-management/#comments</comments>
		<pubDate>Thu, 05 Feb 2009 14:01:01 +0000</pubDate>
		<dc:creator>Nathanael Jones</dc:creator>
				<category><![CDATA[ASP.NET]]></category>
		<category><![CDATA[PageClass]]></category>

		<guid isPermaLink="false">http://66.29.219.39/?p=152</guid>
		<description><![CDATA[Adding simple metadata and stylesheet management to the Page class.]]></description>
			<content:encoded><![CDATA[<p>Adding stylesheets or changing meta tags is just <em>slighly</em> clunky with the default Page class provided by ASP.NET. We&#8217;re going to fix that.</p>
<p>Here&#8217;s how to add metadata and stylesheet management by making a subclass which we&#8217;ll call PageBase, since all markup pages will inherit from it.</p>
<h2>Features</h2>
<ul class="normallist">
<li>One-line method calls for  common operations like adding a stylsheet reference or changing metadata. </li>
<li>Duplicate stylsheet/meta tag prevention.</li>
<li>Only 2 instance members will be added to PageBase &#8211; .Stylesheets and .Metadata.<br />
Stylesheet (HtmlLink) and Metadata (HtmlMeta) management is encapsulated in separate classes to keep things clean, and reduce methods inside the page class.</li>
<li>Static PageBase.GetControlsOfType&lt;t>(Control parent) method simplifies all kinds of hierarchy querying.</li>
</ul>
<h2>StylesheetManager methods</h2>
<p>Complete documentation is in the XML comments in the code.</p>
<pre name="code" class="c-sharp">
Page.Stylesheets.AddLink("~/css/effects/css","stylesheet", "text/css");
Page.Stylesheets.AddLink("~/css/effects/css"); //Shorter syntax for typcial usage
Page.Stylesheets.AddLinkIfMissing("~/css/effects/css"); //So User Controls and multi-instance objects can safely include stylesheets without worrying about duplicates.
Page.Stylesheets.RemoveLinks("~/css/effects/css"); //Removes all links matching this path (ResolveUrl() is called on all paths prior to comparison)
Page.Stylesheets.GetControls() //returns all HtmlLink controls within the page header.
Page.Stylesheets.GetHrefs() //Returns a collection of all hrefs from the link tags on the page.
Page.Stylesheets.FindLinkControl(string href); //Case-insensitive, ResolveURL-cleaned search by href value.
</pre>
<h2>MetadataManager methods</h2>
<pre name="code" class="c-sharp">
Page.Metadata[&quot;description&quot;] = &quot;Why everybody ends up extending the Page class&quot;.
if (Page.Metadata[&quot;description&quot;] == &quot;Why everybody ends up extending the Page class&quot;){}

// Returns a collection of all HtmlMeta controls in the header
// Calls  PageBase.GetControlsOfType&lt;HtmlMeta&gt;(_page.Header);
Page.Metadata.GetControls();
Page.Metadata.GetNameContentPairs(); //Returns a NameValueCollection of the metadata name/content pairs.
Page.Metadata.GetControl(&quot;description&quot;) // Returns the first HtmlMeta instance matching the specified name attribute
Page.Metadata.RemoveControls(List&lt;HtmlMeta&gt;) //Detaches each item in the collection from its parent
Page.Metadata.HideControls(List&lt;HtmlMeta&gt;) //Sets .Visible and .EnableViewState to &quot;false&quot; on all items.
Page.Metadata.GetMatches(query) //Returns all HtmlMeta instances with matching Name attributes. Query can be &quot;*&quot; or a comma-delimited list of values like &quot;description,keywords,test&quot;
Page.Metadata.GetNonMatches(quer) //Returns all non-matching HtmlMeta instances
</pre>
<h2>Usage</h2>
<p>We can use this base class in any page my making a single change.<br />
If the page has a code-behind file, just change </p>
<pre name="code" class="c-sharp">
public partial class _Default : System.Web.UI.Page

to

public partial class _Default : fbs.PageBase</pre>
<p>If it is a standalone .aspx file, you can set <code>Inherits="fbs.PageBase"</code>.</p>
<h2>Internals</h2>
<p>All of the querying operations performed only affect controls of a certain type. To greatly simplify the code for both StylesheetManager and MetdataManager, we have added GetControlsOfType&lt;t>(Control parent) to the PageBase class.</p>
<p>This method efficiently builds a collection of all controls in the specified hierarchy that are of type T.</p>
<pre name="code" class="c-sharp">
/// &lt;summary&gt;
/// Iterates over the control structure of the specified object and returns all elements that are
/// of the specified type
/// &lt;/summary&gt;
/// &lt;param name=&quot;parent&quot;&gt;&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public static List&lt;T&gt; GetControlsOfType&lt;T&gt;(Control parent) where T : Control
{
	return GetControlsOfType&lt;T&gt;(parent, false,false);
}
/// &lt;summary&gt;
/// Iterates over the control structure of the specified object and returns all elements that are
/// of the specified type. If there are two items of the specified type, and one is a child of the other,
/// the childrenOnly and parentOnly parameters can be used to control which is selected. If both are false, both controls are returned.
/// &lt;/summary&gt;
/// &lt;param name=&quot;parent&quot;&gt;The control to search&lt;/param&gt;
/// &lt;param name=&quot;childrenOnly&quot;&gt;If true, only the innermost matching children will be returned.&lt;/param&gt;
/// &lt;param name=&quot;parentsOnly&quot;&gt;If true, only the outermost matching parents will be returned.&lt;/param&gt;
/// &lt;returns&gt;&lt;/returns&gt;
public static List&lt;T&gt; GetControlsOfType&lt;T&gt;(Control parent, bool childrenOnly, bool parentsOnly) where T : Control
{
	if (parent == null) return null;
	if (childrenOnly &amp;&amp; parentsOnly) throw
		new ArgumentException(&quot;Only one of childrenOnly and parentsOnly may be true. They are mutually exclusive&quot;);

	//We are doing last-minute initialization to minimize the overhead of building one of these.
	//The List&lt;&gt; constructor should only be called n times, where n is the number of ContentPlaceHolder controls.
	List&lt;T&gt; temp = null;

	if (parent.Controls != null)
	{
		//Loop through all of the child controls
		foreach (Control child in parent.Controls)
		{
			//Recursively search them also.
			List&lt;T&gt; next = GetControlsOfType&lt;T&gt;(child,childrenOnly,parentsOnly);

			//To save on initialization costs.
			if (next != null)
			{
				if (temp == null)
				{
					temp = next; //Use existing collection from recursive call
				}
				else
				{
					//Merge the collections

					//If a the same object is the child of two different parents, this will
					//stop it.
					foreach (T c in next)
					{
						if (!temp.Contains(c)) temp.Add(c);
					}

				}
			}
		}
	}

	//If this item is of the target type, add it
	if ((parent is T))
	{
		//If there are no children or we are trying to discard children
		if (parentsOnly || temp == null)
		{
			//Clear the list and add the parent
			T item = (T)parent;

			temp = new List&lt;T&gt;();

			temp.Add(item);
		}
		else if (!childrenOnly)
		{
			//Append the parent with the children
			T item = (T)parent;

			if (temp == null) temp = new List&lt;T&gt;();

			temp.Add(item);
		}
	}

	return temp;
}
</pre>
<p>The remainder of the PageBase class</p>
<pre name="code" class="c-sharp">
/// <summary>
/// Extends System.Web.UI.Page
/// Adds metadata and stylehseet management
/// </summary>
public partial class PageBase : Page
{

/// &lt;summary&gt;
/// Creates a new PageBase instance.
/// &lt;/summary&gt;
public PageBase()
{
}

protected MetadataManager _metadata = null;
/// &lt;summary&gt;
/// Manages page metadata. Add, remove, and query metadata
/// (Only meta tags with a name attribute are affected, and only those located in the head section)
/// &lt;/summary&gt;
public MetadataManager Metadata {
	get {
		if (_metadata == null) _metadata = new MetadataManager(this);
		return _metadata;
	}
}

protected LinkManager _Stylesheets = null;
/// &lt;summary&gt;
/// Manages all of the HtmlLink controls in the head section of the page.
/// Register, delete, and enumerate all link tags.
/// &lt;/summary&gt;
public LinkManager Stylesheets
{
	get
	{
		if (_Stylesheets == null) _Stylesheets = new LinkManager(this);
		return _Stylesheets;
	}
}
</pre>
<h2>LinkManager</h2>
<pre name="code" class="c-sharp">

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Specialized;

namespace fbs
{
    public partial class PageBase
    {
        /// &lt;summary&gt;
        /// Manages &amp;lt;link&gt; tags (controls) on the current page.
        /// &lt;/summary&gt;
        public class LinkManager
        {
            protected Page _page = null;
            /// &lt;summary&gt;
            /// Creates a new Link Manager attached to the specified Page instance
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;parent&quot;&gt;&lt;/param&gt;
            public LinkManager(Page parent)
            {
                _page = parent;
            }
            /// &lt;summary&gt;
            /// Adds a CSS reference. Paths must be 1) relative to the page, 2) application-relative, or 3) absolute
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;href&quot;&gt;&lt;/param&gt;
            public void AddLink(string href)
            {
                AddLink(href, &quot;stylesheet&quot;, &quot;text/css&quot;);
            }
            /// &lt;summary&gt;
            /// Adds a CSS stylsheet reference, but only if there isn't one yet for that path.
            /// Paths must be 1) relative to the page, 2) application-relative, or 3) absolute
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;href&quot;&gt;&lt;/param&gt;
            /// &lt;param name=&quot;resolveFirst&quot;&gt;If true, compares resolved paths instead of raw paths&lt;/param&gt;
            public void AddLinkIfMissing(string href)
            {
                if (this.FindLinkControl(href) == null)
                {
                    AddLink(href);
                }
            }
            /// &lt;summary&gt;
            /// Adds a link tag with the specified rel and type attributes
            /// Paths must be 1) relative to the page, 2) application-relative, or 3) absolute
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;href&quot;&gt;&lt;/param&gt;
            /// &lt;param name=&quot;rel&quot;&gt;&lt;/param&gt;
            /// &lt;param name=&quot;type&quot;&gt;&lt;/param&gt;
            public void AddLink(string href, string rel, string type)
            {
                HtmlLink l = new HtmlLink();

                l.EnableViewState = false;
                l.Href = href;
                l.Attributes[&quot;type&quot;] = type;
                l.Attributes[&quot;rel&quot;] = rel;
                l.AppRelativeTemplateSourceDirectory = _page.AppRelativeTemplateSourceDirectory;
                _page.Header.Controls.Add(l);
            }
            /// &lt;summary&gt;
            /// Removes all meta tags with a matching href.
            /// Paths must be 1) relative to the page, 2) application-relative, or 3) absolute
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;href&quot;&gt;&lt;/param&gt;
            /// &lt;param name=&quot;resolveFirst&quot;&gt;If true, compares resolved paths instead of raw paths&lt;/param&gt;
            public void RemoveLinks(string href)
            {
                bool resolveFirst = false;
                List&lt;HtmlLink&gt; controls = GetControls();
                string searchfor = href;
                if (resolveFirst) searchfor = _page.ResolveUrl(searchfor);
                foreach (HtmlLink hl in controls)
                {
                    string thishref = hl.Href;

                    if (resolveFirst) thishref = _page.ResolveUrl(thishref);

                    if (thishref.Equals(searchfor, StringComparison.OrdinalIgnoreCase))
                    {
                        hl.Parent.Controls.Remove(hl);
                    }
                }

            }
            /// &lt;summary&gt;
            /// Returns a collection of all HtmlLink controls in the page header.
            /// &lt;/summary&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;HtmlLink&gt; GetControls()
            {
                return PageBase.GetControlsOfType&lt;HtmlLink&gt;(_page.Header);
            }
            /// &lt;summary&gt;
            /// Returns a collection of the hrefs in each link tag in the head section.
            /// Paths are 1) relative to the page, 2) application-relative, or 3) absolute
            /// &lt;/summary&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;string&gt; GetHrefs()
            {
                List&lt;HtmlLink&gt; list = GetControls();
                List&lt;string&gt; hrefs = new List&lt;string&gt;();
                foreach (HtmlLink l in list)
                {
                    hrefs.Add(l.Href);
                }
                return hrefs;
            }
            /// &lt;summary&gt;
            /// Case-insensitive. Returns the first HtmlLink control in the heirarchy that matches the href. Only scans inside the head tag.
            /// returns null if no match is found.
            ///
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;href&quot;&gt;Paths must be 1) relative to the page, 2) application-relative, or 3) absolute&lt;/param&gt;
            /// &lt;param name=&quot;resolveFirst&quot;&gt;If true, compares resolved paths instead of raw paths&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public HtmlLink FindLinkControl(string href)
            {
                bool resolveFirst = false;
                List&lt;HtmlLink&gt; controls = GetControls();
                string searchfor = href;
                if (resolveFirst) searchfor = _page.ResolveUrl(searchfor);
                foreach (HtmlLink hl in controls)
                {
                    string thishref = hl.Href;

                    if (resolveFirst) thishref = _page.ResolveUrl(thishref);

                    if (thishref.Equals(searchfor, StringComparison.OrdinalIgnoreCase)) return hl;
                }
                return null;
            }

        }
    }
}
</pre>
<h2>MetadataManager</h2>
<pre class="c-sharp" name="code">

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Diagnostics;
using System.Text.RegularExpressions;
using System.Collections.Specialized;

namespace fbs
{
    public partial class PageBase
    {
        /// &lt;summary&gt;
        /// Manages &amp;lt;meta&gt; tags (controls) on the current page. Not designed for HTTP-EQUIV tags - they are ignored and skipped unless they have a name attribute.
        /// Only deals with meta tags within the Head of the page. The page must have a server-side head tag.
        /// &lt;/summary&gt;
        public class MetadataManager
        {
            protected Page _page = null;

            /// &lt;summary&gt;
            /// Creates a new MetadataManager and attaches it to the current page.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;parent&quot;&gt;&lt;/param&gt;

            public MetadataManager(Page parent)
            {
                _page = parent;
            }
            /// &lt;summary&gt;
            /// Gets or sets the Content attribute for the specified metadata tag.
            /// Returns null if pair does not exist.
            /// Creates a new metadata tag if it does not exist.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;name&quot;&gt;&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public string this[string name]
            {

                get
                {
                    HtmlMeta m = FindMetaControl(name, _page.Header);
                    if (m == null) return null;
                    return m.Content;
                }
                set
                {
                    HtmlMeta m = FindMetaControl(name, _page.Header);
                    if (m != null)
                    {
                        m.Content = value;
                    }
                    else
                    {
                        HtmlMeta newm = new HtmlMeta();
                        newm.EnableViewState = false;
                        newm.Name = name;
                        newm.Content = value;
                        _page.Header.Controls.Add(newm);
                    }

                }

            }
            /// &lt;summary&gt;
            /// Returns a collection of all HtmlMeta controls in the header
            /// &lt;/summary&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;HtmlMeta&gt; GetControls()
            {
                return PageBase.GetControlsOfType&lt;HtmlMeta&gt;(_page.Header);
            }
            /// &lt;summary&gt;
            /// Returns a name:value collection of meta name:content pairs from the page.
            /// If there are multiple meta tags with the same name, the contents are comma-delimited (NameValueCollection.Add behavior)
            /// &lt;/summary&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public NameValueCollection GetNameContentPairs()
            {
                List&lt;HtmlMeta&gt; list = PageBase.GetControlsOfType&lt;HtmlMeta&gt;(_page.Header);
                NameValueCollection pairs = new NameValueCollection();
                foreach (HtmlMeta m in list)
                {
                    pairs.Add(m.Name, m.Content);
                }
                return pairs;
            }
            /// &lt;summary&gt;
            /// Returns the first HtmlMeta control with the specified name
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;name&quot;&gt;&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public HtmlMeta GetControl(string name)
            {
                return FindMetaControl(name, this._page.Header);
            }
            /// &lt;summary&gt;
            /// Whether to include or exclude matches
            /// &lt;/summary&gt;
            public enum FilterType
            {
                ReturnMatches = 1,
                ReturnNonMatches = 2
            }
            /// &lt;summary&gt;
            /// Returns all meta tags that don't match 'pattern'
            /// To exclude all, specify &quot;*&quot;. Otherwise, specify a list of exclusions: &quot;date,expires,description,flags&quot;.
            /// Not regex, but case-insensitive.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;pattern&quot;&gt;To exclude all, specify &quot;*&quot;. Otherwise, specify a list of exclusions: &quot;date,expires,description,flags&quot;.&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;HtmlMeta&gt; GetNonMatches(string pattern)
            {
                return GetMatches(pattern, FilterType.ReturnNonMatches);
            }
            /// &lt;summary&gt;
            /// Returns all meta tags with a name that matches 'pattern'
            /// To match all, specify &quot;*&quot;. Otherwise, specify a list of possibilities: &quot;date,expires,description,flags&quot;.
            /// Not regex, but case-insensitive.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;pattern&quot;&gt;To match all, specify &quot;*&quot;. Otherwise, specify a list of possibilities: &quot;date,expires,description,flags&quot;.&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;HtmlMeta&gt; GetMatches(string pattern)
            {
                return GetMatches(pattern, FilterType.ReturnMatches);
            }

            /// &lt;summary&gt;
            /// Removes the specified HtmlMeta controls from their parents.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;list&quot;&gt;&lt;/param&gt;
            public void RemoveControls(List&lt;HtmlMeta&gt; list)
            {
                foreach (HtmlMeta m in list)
                {
                    if (m.Parent != null)
                    {
                        m.Parent.Controls.Remove(m);
                    }
                }
            }

            /// &lt;summary&gt;
            /// Hides the specified HtmlMeta controls from rendering
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;list&quot;&gt;&lt;/param&gt;
            public void HideControls(List&lt;HtmlMeta&gt; list)
            {
                foreach (HtmlMeta m in list)
                {
                    m.Visible = false;
                    m.EnableViewState = false;
                }
            }
            /// &lt;summary&gt;
            /// Returns a collection of HtmlMeta tags that match 'pattern' (or don't match, depending on 'filter').
            /// Pattern is not a regex, but supports alternations and is case-insensitive. if Pattern=&quot;*&quot;, then everything matches.
            /// Pattern can be a single meta name, or a list of meta names (comma or | delimited).
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;pattern&quot;&gt;To match all, specify &quot;*&quot;. Otherwise, specify a list of possibilities: &quot;date,expires,description,flags&quot;.&lt;/param&gt;
            /// &lt;param name=&quot;filter&quot;&gt;&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            public List&lt;HtmlMeta&gt; GetMatches(string pattern, FilterType filter)
            {

                //List of all meta controls in the head
                List&lt;HtmlMeta&gt; list = PageBase.GetControlsOfType&lt;HtmlMeta&gt;(_page.Header);

                //Parse pattern string
                bool wildcard = (pattern.Equals(&quot;*&quot;, StringComparison.OrdinalIgnoreCase));

                string[] parts = pattern.Replace(',', '|').Split('|');
                for (int i = 0; i &lt; parts.Length; i++)
                    parts[i] = parts[i].Trim().ToLowerInvariant();

                //Index valid names in a binary tree
                List&lt;string&gt; names = new List&lt;string&gt;(parts);
                names.Sort();

                //Create collections to hold matches and non-matches.
                List&lt;HtmlMeta&gt; matches = new List&lt;HtmlMeta&gt;();
                List&lt;HtmlMeta&gt; nonmatches = new List&lt;HtmlMeta&gt;();

                //Loop throught controls and distribute to the appropriate collection.
                foreach (HtmlMeta m in list)
                {
                    //Skip meta tags with an no name (probably HTTP-EQIV)
                    if (m.Name == null) continue;

                    if (wildcard)
                    {
                        matches.Add(m);
                    }
                    else if ((names.BinarySearch(m.Name.ToLowerInvariant()) &gt; 0))
                    {
                        matches.Add(m);
                    }
                    else
                    {
                        nonmatches.Add(m);
                    }
                }

                //Return the correct collection based upon the filter type
                if (filter == FilterType.ReturnMatches) return matches;
                else if (filter == FilterType.ReturnNonMatches) return nonmatches;
                else throw new ArgumentException(&quot;filter must be a valid enumeration value&quot;, &quot;filter&quot;);
            }
            /// &lt;summary&gt;
            /// Recursively searches the hierarchy of 'parent' for the first HtmlMeta instance with the specified Name attribute.
            /// Case-insensitive.
            /// &lt;/summary&gt;
            /// &lt;param name=&quot;name&quot;&gt;Case-insensitive. &lt;/param&gt;
            /// &lt;param name=&quot;parent&quot;&gt;Control tree to search&lt;/param&gt;
            /// &lt;returns&gt;&lt;/returns&gt;
            protected static HtmlMeta FindMetaControl(string name, Control parent)
            {
                if (parent is HtmlMeta)
                {
                    HtmlMeta m = parent as HtmlMeta;
                    if (m.Name.Equals(name, StringComparison.OrdinalIgnoreCase)) return m;
                }
                foreach (Control c in parent.Controls)
                {
                    HtmlMeta m = FindMetaControl(name, c);
                    if (m != null) return m;
                }
                return null;
            }

        }
    }
}
</pre>
<h2>Integrating script support</h2>
<p>If you haven&#8217;t already read <em><a href="~/11011" runat="server">Referencing stylesheets and scripts from content pages</a></em>, give it a glance. Download the attached code files and combine them with this one. The only file you&#8217;ll have to merge is PageBase.cs. </p>
<p>Just add this code to the PageBase.cs included in the article, and make sure ContentPlaceHolderFixes.cs and the ScriptReference.cs files are included also.</p>
<pre class="c-sharp" name="code">
/// &lt;summary&gt;
/// Calls the ContentPlaceHolderHeadRepair.Repair method
/// &lt;/summary&gt;
/// &lt;param name=&quot;e&quot;&gt;&lt;/param&gt;
protected override void OnLoad(EventArgs e)
{
	//Parses link, meta, and script tags that got missed by the Head&gt;CPH bug.
	ContentPlaceHolderFixes.RepairPageHeader(this);

	//Fire the events later
	base.OnLoad(e);
}
</pre>
<p>This will allow you to use script tags from the head section and get proper ASP.NET URL resolution on them.
<p>If you have any questions, please use the comments form below.</p>
<p>Happy coding!</p>
<p><!--Todo: use IEnumerable instead of List when possible--></p>
<p>
<a href='http://nathanaeljones.com/wp-content/uploads/2009/02/MetadataStylesheetManagement.zip'>Download files</a></p>
]]></content:encoded>
			<wfw:commentRss>http://nathanaeljones.com/152/extending-page-adding-metadata-and-stylesheet-management/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Types of ASP.NET paths</title>
		<link>http://nathanaeljones.com/129/types-of-asp-net-paths/</link>
		<comments>http://nathanaeljones.com/129/types-of-asp-net-paths/#comments</comments>
		<pubDate>Mon, 09 Jun 2008 11:00:12 +0000</pubDate>
		<dc:creator>Nathanael Jones</dc:creator>
				<category><![CDATA[ASP.NET]]></category>

		<guid isPermaLink="false">http://66.29.219.39/?p=129</guid>
		<description><![CDATA[ASP.NET is primarily concerned with "virtual paths", the portion of the path following the hostname or port number. When working with ASP.NET, you must understand the following types of URIs thoroughly, and know how they are handled by ASP.NET and the browser.]]></description>
			<content:encoded><![CDATA[<p><!-- #content ul{  list-style-type:disc;  padding-left:20px; } span.examplePath{ color:green; } --></p>
<p>ASP.NET is primarily concerned with &#8220;virtual paths&#8221;, the portion of the path following the hostname or port number. When working with ASP.NET, you must understand the following types of URIs thoroughly, and know how they are handled by ASP.NET and the browser.</p>
<ul>
<li> <strong>Absolute paths.</strong> Ex. <span class="examplePath">http://mycomputer/Web1/Test/images/companylogo.png</span>
<ul>
<li>ASP.NET leaves this type of path alone – it&#8217;s already in the least ambiguous form possible. Browsers understand absolute paths very well.</li>
<li>Only use these for referencing external websites. They&#8217;re expensive to maintain.</li>
</ul>
</li>
<li><strong>Root-relative virtual paths.</strong> Ex. <span class="examplePath">/Web1/Test/images/companylogo.png</span>
<ul>
<li> ASP.NET leaves these alone too. Browsers resolve the path client-side by combining it with the domain of the parent document.</li>
<li>I don&#8217;t ever recommend hard-coding these into a website &#8211; use application-relative paths or relative paths instead. <strong>Note: These are also called &#8220;absolute virtual paths&#8221; and &#8220;domain-relative paths&#8221;.</strong></li>
</ul>
</li>
<li><strong>Application-relative paths.</strong> Ex. <span class="examplePath">~/images/companylogo.png</span>
<ul>
<li>Browsers don&#8217;t have a clue what the tilde(~) means, so server-side path resolution is required. Server-side, the tilde is shorthand for HttpRuntime.AppDomainAppVirtualPath.</li>
<li>ASP.NET rebases these as client-side relative paths on some control attributes, but you must remember to use <strong>runat=&#8221;server&#8221;</strong>.</li>
<li>This is the type of path you should use if a relative path doesn&#8217;t make sense.</li>
</ul>
</li>
<li><strong>Relative paths.</strong> Ex: <span class="examplePath">../images/logo.png</span>
<ul>
<li> There are two types of relative paths: server-side and client-side. They aren&#8217;t syntactically different, but server-side paths are relative to the containing source file, and client-side paths are relative to the address bar or parent markup file.</li>
<li>Server-side relative paths are assumed to be relative to the containing .master, .ascx, or .aspx file location. These must be rebased into client-side relative paths when rendered using ResolveClientUrl(). Most ASP.NET controls do this for you. You should use this type of path whenever you are referencing a related file that won&#8217;t move in relation to the current file.</li>
<li>Client-side relative paths are relative to the parent URL, usually the address bar. If you want to reference an image on an html page, you must use a path that is relative to the address bar location of the html page. If you want to reference a image from within a .css file, you must use a path that is <strong>relative to the .css file.</strong> Paths inside javascript files are <strong>not</strong> relative to the javascript source location, though. They must be relative to the document the script is executing in, the address bar.</li>
</ul>
</li>
<li><strong>Fragment and Javascript paths.</strong> Ex. <span class="examplePath">#section2</span> or <span class="examplePath">javascript:OpenPopup();</span>
<ul>
<li>ASP.NET leaves these alone. The browser is not supposed to create a new request when one of these is clicked, but to simply perform the action or navigation within the current document.</li>
<li> Fragments never appear in a HTTP request. They are only for the browser&#8217;s benefit, and are stripped off before the path is sent to ASP.NET.</li>
</ul>
</li>
</ul>
<p>Note &#8211; When I say that ASP.NET leaves a path alone, I mean that it doesn&#8217;t do any path rebasing. ASP.NET will still do some attribute encoding on the path if it is embedded in a hyperlink.</p>
]]></content:encoded>
			<wfw:commentRss>http://nathanaeljones.com/129/types-of-asp-net-paths/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Incoming request paths</title>
		<link>http://nathanaeljones.com/113/incoming-request-paths/</link>
		<comments>http://nathanaeljones.com/113/incoming-request-paths/#comments</comments>
		<pubDate>Fri, 16 May 2008 15:30:57 +0000</pubDate>
		<dc:creator>Nathanael Jones</dc:creator>
				<category><![CDATA[ASP.NET]]></category>

		<guid isPermaLink="false">http://66.29.219.39/?p=113</guid>
		<description><![CDATA[Casing and URL encoding add significant complexity to the jobs IIS and ASP.NET must perform. ASP.NET automatically decodes and lowercases the scheme, host, and port. The Path (and PathInfo) portions are decoded, but case is not changed.]]></description>
			<content:encoded><![CDATA[<style type="text/css">
dt { float:left; display:inline; padding-right:25px; font-weight:bold;}
dd {  }
h3 {margin-top:2em;}</p>
<p>dl{
border:1px solid #eaeaea;
padding:15px;
margin-top:15px;
font-size:8pt;
overflow:scroll;
}
span.querystring {
color:red;
}</p>
<p>span.pathinfo{
color:green;
}</p>
<p>span.filepath{
color:blue
}</p>
<p>table.rootVsSubfolder{
font-size:7.5pt;
color:black;</p>
<p>}
table.rootVsSubfolder td{
border-top: 1px solid gray;
padding:5px;
text-align:center;
}
table.rootVsSubfolder thead td{</p>
<p>font-weight:bold;
}
td.property{
border-right: 1px solid gray;
padding-left:0px;
}
td.root{
border-right: 1px solid gray;
}
td.subfolder{
padding-right:0px;
}
</style>
<h2>Casing and URL encoding</h2>
<p>Casing and URL encoding add significant complexity to the jobs IIS and ASP.NET must perform. ASP.NET automatically decodes and lowercases the scheme, host, and port. The Path (and PathInfo) portions are decoded, but case is not changed. The querystring is not modified. When the querystring is parsed to populate the QueryString collection, names and values get URL decoded once.</p>
<p>There are an almost infinite number of ways to represent every URI &ndash; take for instance the following URIs, and how they are sanitized before your application receives them:</p>
<h3>URL encoded scheme (http)</h3>
<dl>
<dt>URI</dt>
<dd>
%68%74%74%70://localhost:87/content/pages/test.aspx</dd>
<dt>Response</dt>
<dd>IIS7 returns HTTP Error 400.0 &#8211; Bad Request</dd>
</dl>
<h3>Double URL encoded host</h3>
<dl>
<dt>URI</dt>
<dd>http://%25%34%63%25%34%66%25%34%33%25%34%31%25%34%63%25%34%38%25%34%66%25%35%33%25%35%34:87/Content/pages/test.aspx</dd>
<dt>Response</dt>
<dd>IE7 returns &#8220;Host not found&#8221; (browser only performs one level of URL decoding, apparently)</dd>
</dl>
<h3>URL Encoded host (UPPERCASE)</h3>
<dl>
<dt>URI</dt>
<dd>http://%4c%4f%43%41%4c%48%4f%53%54:87<span class="filepath">/Content/pages/test.aspx</span></dd>
<dt>Request.Url.OriginalString</dt>
<dd>http://localhost:87<span class="filepath">/Content/pages/test.aspx</span></dd>
</dl>
<p>IE7 decodes the host name automatically</p>
<h3>URL Encoding the host (lowercase)</h3>
<dl>
<dt>URI</dt>
<dd>Error! Hyperlink reference not valid.</dd>
<dt>Request.Url.OriginalString</dt>
<dd>http://localhost:87/Content/pages/test.aspx</dd>
</dl>
<h3>Mixed case</h3>
<dl>
<dt>URI</dt>
<dd>HttP://LOcalHoSt:87/<span class="filepath">CoNtEnT/pAgEs/tEsT.asPx</span></dd>
<dt>Request.Url.OriginalString</dt>
<dd>http://localhost:87/<span class="filepath">CoNtEnT/pAgEs/tEsT.asPx</span></dd>
</dl>
<h3>URLEncoding the entire URI (http://localhost:87/content/pages/test.aspx)</h3>
<dl>
<dt>URI</dt>
<dd>%68%74%74%70%3a%2f%2f%6c%6f%63%61%6c%68%6f%73%74%3a%38%37%2f%63%6f%6e%74%65%6e%74%2f%70%61%67%65%73%2f%74%65%73%74%2e%61%73%70%78</dd>
<dt>Respnse</dt>
<dd>IIS7 returns HTTP Error 400.0 &#8211; Bad Request</dd>
</dl>
<h3>URL Encoding the path (FilePath + PathInfo), but not the query string</h3>
<dl>
<dt>URI</dt>
<dd>http://localhost<span class="filepath">/%55%72%6c%54%65%73%74%73%2f%63%6f%6e%74%65%6e%74%2f%70%61%67%65%73%2f%74%65%73%74%2e%61%73%70%78</span><span class="pathinfo">%2f%66%6f%6c%64%65%72%2f%66%69%6c%65</span><span class="querystring">?querystring</span></dd>
<dt>Request.Url.OriginalString</dt>
<dd>http://localhost:80<span class="filepath">/UrlTests/content/pages/test.aspx</span><span class="pathinfo">/folder/file</span><span class="querystring">?querystring</span></dd>
</dl>
<h3>URL Encoding the path, querystring, and the &#8220;?&#8221; in-between.</h3>
<dl>
<dt>URI</dt>
<dd>http://localhost/%55%72%6c%54%65%73%74%73%2f%63%6f%6e%74%65%6e%74%2f%70%61%67%65%73%2f%74%65%73%74%2e%61%73%70%78%2f%66%6f%6c%64%65%72%2f%66%69%6c%65%3f%71%75%65%72%79%73%74%72%69%6e%67
</dd>
<dt>Response</dt>
<dd>
<pre>Server Error in '/UrlTests' Application.
'/UrlTests/content/pages/test.aspx/folder/file?querystring' is not a valid virtual path.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.
Exception Details: System.Web.HttpException: '/UrlTests/content/pages/test.aspx/folder/file?querystring' is not a valid virtual path.
Source Error:
Frame1
Source File: c:\Users\Nathanael\desktop\webapplication2\Content\pages\test.aspx.cs    Line: 912308
Stack Trace:
Frame2
Version Information: Microsoft .NET Framework Version:2.0.50727.312; ASP.NET Version:2.0.50727.833
</pre>
</dd>
</dl>
<pre>
Hackers can exploit this to discover:
   If CustomErrors=false
   1.The physical location of the web site
   2.The physicial loation of the .NET framework and the
Of course, I'm sure there are other ways to generate errors - this is just one you can't patch.
</pre>
<p>Also, since the entire URL is parsed as the virtual path, it looks like this is a way to circumvent the splitting of FilePath, PathInfo, and the querystring, since the whole URL was parsed as the virtual path.</p>
<p>If you want the querystring to be parsed correctly, you cannot encode the delimiting question mark.</p>
<h3>URL Encoding the path and querystring separately</h3>
<dl>
<dt>URI</dt>
<dd>http://localhost<span class="filepath">/%55%72%6c%54%65%73%74%73%2f%63%6f%6e%74%65%6e%74%2f%70%61%67%65%73%2f%74%65%73%74%2e%61%73%70%78</span><span class="pathinfo">%2f%66%6f%6c%64%65%72%2f%66%69%6c%65</span><span class="querystring">?%71%75%65%72%79%73%74%72%69%6e%67</span></dd>
<dt>Request.Url.OriginalString</dt>
<dd>http://localhost:80<span class="filepath">/UrlTests/content/pages/test.aspx</span><span class="pathinfo">/folder/file</span><span class="querystring">?%71%75%65%72%79%73%74%72%69%6e%67</span></dd>
</dl>
<p>Note: HttpUtility.URLEncode() only encodes non-alphanumeric characters. You&#8217;ll have to use something else to get complete encoding, such as is used above.</p>
<p>So, I think you can see why basing security on the incoming path is a bad idea. </p>
<h2>Differences between Root and Subfolder applications</h2>
<p>IIS allows websites to be hosted in virtual folders that do not correspond to the on-disk organization. For example, if you publish a website located in C:\Websites\MyWebsite2\ on virtual folder /Web1/Test/, The URI to access it would be http://mycomputer/Web1/Test/.</p>
<p>The following table contains data from single web site being accessed from two different locations. One channel is the virtual folder /URLTests on port 80, and the other is the root website on port 87.</p>
<table class="rootVsSubfolder" cellpadding="0" cellspacing="0">
<thead>
<tr>
<td class="property">
Property</td>
<td class="root" width="100px">http://localhost:87/content/pages/ test.aspx/pathinfo?query=value</td>
<td class="subfolder">http://localhost/UrlTests/content/pages/ test.aspx/pathinfo?query=value</td>
</tr>
</thead>
<tbody>
<tr>
<td class="property">	Request. ApplicationPath	</td>
<td class="root">	/	</td>
<td class="subfolder">	/UrlTests	</td>
</tr>
<tr>
<td class="property">	Request. RawUrl	</td>
<td class="root">	/content/Pages/test.aspx/pathinfo?query=value	</td>
<td class="subfolder">	/UrlTests/content/Pages/test.aspx/pathinfo?query=value	</td>
</tr>
<tr>
<td class="property">	Request. AppRelativeCurrent ExecutionFilePath	</td>
<td class="root">	~/content/Pages/test.aspx	</td>
<td class="subfolder">	~/content/Pages/test.aspx	</td>
</tr>
<tr>
<td class="property">	Request.Current ExecutionFilePath	</td>
<td class="root">	/content/Pages/test.aspx	</td>
<td class="subfolder">	/UrlTests/content/Pages/test.aspx	</td>
</tr>
<tr>
<td class="property">	Request. FilePath	</td>
<td class="root">	/content/Pages/test.aspx	</td>
<td class="subfolder">	/UrlTests/content/Pages/test.aspx	</td>
</tr>
<tr>
<td class="property">	Request. Path	</td>
<td class="root">	/content/Pages/test.aspx/pathinfo	</td>
<td class="subfolder">	/UrlTests/content/Pages/test.aspx/pathinfo	</td>
</tr>
<tr>
<td class="property">	Request. PathInfo	</td>
<td class="root">	/pathinfo	</td>
<td class="subfolder">	/pathinfo	</td>
</tr>
<tr>
<td class="property">	Request. PhysicalApplicationPath	</td>
<td class="root">	C:\Users\Nathanael\Desktop\WebApplication2\	</td>
<td class="subfolder">	C:\Users\Nathanael\Desktop\WebApplication2\	</td>
</tr>
<tr>
<td class="property">	Request. PhysicalPath	</td>
<td class="root">	C:\Users\Nathanael\Desktop\WebApplication2 \content\Pages\test.aspx	</td>
<td class="subfolder">	C:\Users\Nathanael\Desktop\WebApplication2 \content\Pages\test.aspx	</td>
</tr>
<tr>
<td class="property">	Request. Url.OriginalString	</td>
<td class="root">	http://localhost:87/content/Pages/ test.aspx/pathinfo?query=value	</td>
<td class="subfolder">	http://localhost:80/UrlTests/content/Pages/ test.aspx/pathinfo?query=value	</td>
</tr>
</tbody>
</table>
<p>
Note: CurrentExecutionFilePath (and the AppRelative version) both differ from FilePath with Server.Execute or Server.Transfer is used. CurrentExecutionFilePath changes to reflect the executing file, while FilePath remains unchanged. Spaces in the table above were inserted to allow wrapping of the text &#8211; the actual data contains no whitespace.
</p>
<p>This should illustrate why application-relative paths (~/file.aspx) should be used wherever possible &#8211; they permit the site to be hosted in virtual folder as well as in a site root. Can you imaging the maintenance costs if you needed to move your site, and were using absolute paths, such as (/vdir/file.aspx)? You may not currently need to host the site on a virtual folder for dev purposes, but what about 5 years from now when you want to keep the old system online in a subfolder of the new site?</p>
]]></content:encoded>
			<wfw:commentRss>http://nathanaeljones.com/113/incoming-request-paths/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
