Wednesday, October 6, 2010

ASP.NET - Convert Relative URL to Absolute (Part 2)

This is a follow up to my previous post: ASP.NET - Convert Relative URL to Absolute.

Twitter user @GeertDoornbos alerted me to a more concise way to convert a relative URL to an absolute which offers the same conversion functionality, but works with file-system relative paths (/path/to/foo or ../path/to/foo) instead of app-relative paths (~/path/to/foo). Check it out:

public static string ToAbsoluteUrl(this string relativeUrl) {
    if(HttpContext.Current == null || string.IsNullOrEmpty(relativeUrl)) 
        return relativeUrl;
    
    return new Uri(HttpContext.Current.Request.Url, relativeUrl).ToString();
}

It should be noted that my technique and this one are not interchangable because app-relative paths have a different meaning to the Uri object than file system relative paths. Namely, if the current Request.Url were equal to 'http://www.website.com/foo/bar' and I passed the Uri constructor `~/path/to/file` it would interpret the path as 'http://www.website.com/foo/bar/~/path/to/file` instead of 'http://www.website.com/path/to/file`.

Thanks again for the tip @GeertDoornbos.

Thursday, July 8, 2010

ASP.NET – Convert Relative URL to Absolute

See my follow up post for an alternative ToAbsoluteUrl() which accepts file-system-relative paths instead of app-relative paths: ASP.NET – Convert Relative URL to Absolute (Part 2).

Having built a blogging engine into my content management system at work, one thing I have to deal with in terms of the blog is making sure that any blog posts with references to website content make use of a fully-qualified, absolute URLs when they are rendered in the RSS feed.

Here’s two extension methods that will convert relative URLs into absolute URLs which include the full host name and port (if applicable). The first method will take a single app-relative URL (~/path/to/foo) and fully-qualify it, while the second method accepts a block of HTML and uses regular expressions to match HTML attributes which contain root-relative URLs (/path/to/foo) and replaces them using the first method.

public static string ToAbsoluteUrl(this string relativeUrl) {
    if (string.IsNullOrEmpty(relativeUrl))
        return relativeUrl;

    if (HttpContext.Current == null)
        return relativeUrl;

    if (relativeUrl.StartsWith("/"))
        relativeUrl = relativeUrl.Insert(0, "~");
    if (!relativeUrl.StartsWith("~/"))
        relativeUrl = relativeUrl.Insert(0, "~/");

    var url = HttpContext.Current.Request.Url;
    var port = url.Port != 80 ? (":" + url.Port) : String.Empty;

    return string.Format("{0}://{1}{2}{3}", url.Scheme, url.Host, port, VirtualPathUtility.ToAbsolute(relativeUrl));
}
public static string HtmlAppRelativeUrlsToAbsoluteUrls(this string html) {
    if (string.IsNullOrEmpty(html))
        return html;

    const string htmlPattern = "(?<attrib>\\shref|\\ssrc|\\sbackground)\\s*?=\\s*?"
                              + "(?<delim1>[\"'\\\\]{0,2})(?!#|http|ftp|mailto|javascript)"
                              + "/(?<url>[^\"'>\\\\]+)(?<delim2>[\"'\\\\]{0,2})";

    var htmlRegex = new Regex(htmlPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
    html = htmlRegex.Replace(html, m => htmlRegex.Replace(m.Value, "${attrib}=${delim1}" + ("~/" + m.Groups["url"].Value).ToAbsoluteUrl() + "${delim2}"));

    const string cssPattern = "@import\\s+?(url)*['\"(]{1,2}"
                              + "(?!http)\\s*/(?<url>[^\"')]+)['\")]{1,2}";

    var cssRegex = new Regex(cssPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);
    html = cssRegex.Replace(html, m => cssRegex.Replace(m.Value, "@import url(" + ("~/" + m.Groups["url"].Value).ToAbsoluteUrl() + ")"));

    return html;
}

Sources

Monday, June 21, 2010

ASP.NET - Redirect Module

As developers we are all intimately aware of the implications of changing the URL of existing content in our applications which have already been indexed by search engines and users. It is never enough to simply change a URL and update the links because old pages, bookmarks, or search results may still point to the original URL. That being so, it is imperative to add an appropriate HTTP 301 redirect for the original URL when modifying the location of existing content.

In Apache-based web servers adding a 301 Redirect is as simple as adding a .htaccess file with the appropriate mod_rewrite instructions; unfortunately, this simplicity of configuration is unavailable on an IIS server and when an application requires a redirect be added we are forced to either edit the IIS settings explicitly or add the redirect to the application’s code. Ultimately, the former solution is ideal because it allows IIS to bypass the ASP.NET request engine and immediately serve the redirect, but for many of us, editing the IIS configuration is not an option due to the use of shared hosting environments. In ASP.NET code, issuing a 301 Redirect in your Global.asax looks something like this:

protected void Application_BeginRequest(object sender, EventArgs e) {
    var oldUrl = new Uri("http://www.website.com/path/to/content");
    var newUrl = new Uri("http://www.website.com/new/path/to/content");

    if(string.Equals(Request.Url.AbsolutePath, oldUrl.AbsolutePath, StringComparison.OrdinalIgnoreCase)) {
        Response.StatusCode = 301;
        Response.Status = "301 Moved Permanently";
        Response.RedirectLocation = newUrl;
        Response.End();
    }
}

While searching for a more maintainable solution I came across a blog post by Scott Hanselman which directed me to a HttpModule written by Fritz Onion. The module developed is elegant and effective because it uses a special section in the website's configuration file to define each redirect, thus minimizing maintenance challenges associated with adding and removing individual redirects. That being said, Fritz's solution is dated and uses some obsolete code so I have gone ahead and modernized it a bit. In addition to the original feature set, I have added support for matching the entire request URL.

Issuing redirects with the module is as easy as registering and adding the <redirections /> configuration section and then declaring each redirect rule.

<?xml version="1.0"?>
<configuration>
  <configSections>
    <section name="redirections" type="RedirectModule.Configuration.RedirectionsSection, RedirectModule"/>
  </configSections>
  <redirections>
    <add targetUrl="^http://website.com" destinationUrl="http://www.website.com" ignoreCase="true" />
    <add targetUrl="^~/FalseTarget.aspx" destinationUrl="~/RealTarget.aspx" ignoreCase="true"/>
    <add targetUrl="^~/2ndFalseTarget.aspx" destinationUrl="~/RealTarget.aspx" permanent="true"/>
    <add targetUrl="^~/(Author|Category|Tool)([A-Za-z0\d]{8}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{4}-?[A-Za-z\d]{12}).aspx$" destinationUrl="~/Pages/$1.aspx?$1=$2"/>
    <add targetUrl="^~/SomeDir/(.*).aspx\??(.*)" destinationUrl="~/Pages/$1/Default.aspx?$2"/>
  </redirections>
  <system.web>
    <httpModules>
      <add name="RedirectModule" type="RedirectModule.RedirectModule"/>
    </httpModules>
  </system.web>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="RedirectModule" type="RedirectModule.RedirectModule, RedirectModule"/>
    </modules>
  </system.webServer>
</configuration>

Sources

Update: 03/26/12

I've made a few minor tweaks in the module's code. The library now targets .NET 2.0 for maximum compatibility and the sample project works much better out of the box.

Tuesday, June 8, 2010

ASP.NET MVC Flags Enumeration Model Binder

The default model binder that ships with ASP.NET MVC 2 handles the large majority of databinding scenarios; however, one feature it does not support is combining a set of flags enumeration values into a single value. With the default model binder if you try to post a form with multiple enumeration values for a single model item, the returned model will contain only the last enumeration value provided by the form.

To solve this problem I created a FlagsEnumerationModelBinder to automatically handle the bitwise-or operation required to combine the enumeration values. Given a controller action that receives a model item of an enumeration type with the FlagsAttribute set, on BindModel() the FlagsEnumerationModelBinder will first check that there is more than one enumeration value in the request and then bitwise-or each individual value into a single byte value which is then returned in a call to Enum.Parse(Type, String).

public class FlagEnumerationModelBinder : DefaultModelBinder
{
public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
if (bindingContext == null) throw new ArgumentNullException("bindingContext");

if (bindingContext.ValueProvider.ContainsPrefix(bindingContext.ModelName)) {
var values = GetValue<string[]>(bindingContext, bindingContext.ModelName);

if (values.Length > 1 && (bindingContext.ModelType.IsEnum && bindingContext.ModelType.IsDefined(typeof(FlagsAttribute), false))) {
long byteValue = 0;
foreach (var value in values.Where(v => Enum.IsDefined(bindingContext.ModelType, v))) {
byteValue |= (int)Enum.Parse(bindingContext.ModelType, value);
}

return Enum.Parse(bindingContext.ModelType, byteValue.ToString());
}
else {
return base.BindModel(controllerContext, bindingContext);
}
}

return base.BindModel(controllerContext, bindingContext);
}

private static T GetValue<T>(ModelBindingContext bindingContext, string key) {
if (bindingContext.ValueProvider.ContainsPrefix(key)) {
ValueProviderResult valueResult = bindingContext.ValueProvider.GetValue(key);
if (valueResult != null) {
bindingContext.ModelState.SetModelValue(key, valueResult);
return (T)valueResult.ConvertTo(typeof(T));
}
}
return default(T);
}
}

The FlagsEnumerationModelBinder assumes that each enumeration value in the request has the same field name. The values can be in string or numeric form.

Careful about using the Html.CheckBox() extension to select your enumeration values in the form because the CheckBox and RadioButton helpers output an additional hidden value for each field that is sent with the request whether or not the field is checked. If you use the helpers you’ll end up with “true” and “false” values in the request which are [probably] not part of your enumeration.

Tuesday, May 11, 2010

C# Truncate String

To preserve whole words when truncating, try this version: C# Truncate String - Whole Words

A simple extension method to safely truncate any string to a predetermined length:

public static string Truncate(this string str, int maxLength) {
    if (str == null) return null;
    return str.Substring(0, Math.Min(maxLength, str.Length));
}

Just call string.Truncate(int) and the returned string will be trimmed accordingly.

Thursday, April 22, 2010

How to Make a Tag Cloud in ASP.NET MVC

As many of you are aware, tagging offers users a quick and easy way to filter website content by interesting topics or categories and a tag cloud acts as a visually asethetic way to display links to all of the available tags with their prevelance indicated by font-size. Lately tag clouds have become very popular and they can be found all over the web in many different formats, but the most common format usually looks something like this:

tagcloud Thanks, Phil Haacked. :)

In this tutorial I will show you how to make a simple tag cloud with 5 different size distributions based on the frequency of each tag. The code I have built is contained inside of an HtmlHelper which accepts a Dictionary<string, int> containing the tags and their occurrence counts in each pair and a Func<string, string> which is called on each tag to produce the link which will be displayed. Before we build the tag cloud, we’ll need to get the tags out of our database along with their number of occurrences. This code will obviously vary based on your tagging implementation, but will probably look something like this:

(from tag in Tags 
 group tag by tag.Name 
 into g 
 select g).ToDictionary(g => g.Key, g => g.Count());

Once we have the tag list in hand we need to find the mimimum and maximum tag counts and establish a distribution between the difference of the minmum and maximum values.

var min = tagList.Min(t => t.Value);
var max = tagList.Max(t => t.Value);
var dist = (max - min) / 3;

The code for this tag cloud supports a total of 5 different sizes, but if you require more you can increase the divisor of the distribution equation and update the CSS class assignment logic accordingly. With the minimum, maximum and distribution values calculated we can now loop through all of the tags and assign CSS classes to each one based on their position in the distribution.

var links = new StringBuilder();
foreach (var tag in tagsAndCounts) {
    string tagClass;
    
    if (tag.Value == max) {
        tagClass = "largest";
    }
    else if (tag.Value > (min + (dist * 2))) {
        tagClass = "large";
    }
    else if (tag.Value > (min + dist)) {
        tagClass = "medium";
    }
    else if (tag.Value == min) {
        tagClass = "smallest";
    }
    else {
        tagClass = "small";
    }

    links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                        urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
}

After running the above code we now have all of the tag links in a StringBuilder which we can push to the output. Notice in the final line of the foreach loop I am calling urlExpression(tag.Key) this is the Func<string, string> I mentioned above that is used to return the Url for the tag.

Now that we have all the pieces, here's the full code for the HtmlHelper:

public static MvcHtmlString TagCloud(this HtmlHelper html, IDictionary<string, int> tagsAndCounts, Func<string, string> urlExpression, object htmlAttributes) {
    if (tagsAndCounts == null || !tagsAndCounts.Any())
        return MvcHtmlString.Empty;
    
    var min = tagsAndCounts.Min(t => t.Value);
    var max = tagsAndCounts.Max(t => t.Value);
    var dist = (max - min) / 3;
    
    var links = new StringBuilder();
    foreach (var tag in tagsAndCounts) {
        string tagClass;
        
        if (tag.Value == max) {
            tagClass = "largest";
        }
        else if (tag.Value > (min + (dist * 2))) {
            tagClass = "large";
        }
        else if (tag.Value > (min + dist)) {
            tagClass = "medium";
        }
        else if (tag.Value == min) {
            tagClass = "smallest";
        }
        else {
            tagClass = "small";
        }

        links.AppendFormat("<a href=\"{0}\" title=\"{1}\" class=\"{2}\">{1}</a>{3}", 
                           urlExpression(tag.Key), tag.Key, tagClass, Environment.NewLine);
    }
    
    var div = new TagBuilder("div");
    div.MergeAttribute("class", "tag-cloud");
    div.InnerHtml = links.ToString();
    
    if (htmlAttributes != null) {
        div.MergeAttributes(new RouteValueDictionary(htmlAttributes), false);
    }
    
    return MvcHtmlString.Create(div.ToString());
}

And here's the appropriate CSS:

.tag-cloud { text-align: center; }
.tag-cloud a { white-space: nowrap; padding: 5px; display: inline-block; }
.tag-cloud a:hover { background-color: #E1E1E1;}
.tag-cloud .smallest { font-size: 75%;}
.tag-cloud .small { font-size: 100%; }
.tag-cloud .medium { font-size: 125%;}
.tag-cloud .large { font-size: 150%; }
.tag-cloud .largest { font-size: 200%; }

In order to use the above code simply place it in an extension method class, reference the CSS, and call it your view like so:

<%= Html.TagCloud(Model.Tags, t => Url.Tag(t), null) %>

Sources

  • The basis for this code comes from Pete Freitag's Blog in a similar post that discusses building a tag cloud with ColdFusion.

Tuesday, April 20, 2010

System.ComponentModel.DataAnnotations Validation Attributes – Using Error Message Resource Files

Just a quick-tip if you’ve been making use of the wonderful set of model validation attributes available in System.ComponentModel.DataAnnotations.

If you elect to use the ValidationAttribute’s ErrorMessageResourceName and ErrorMessageResourceType properties, make sure the resource file type you point to is marked with a Public access modifier and not the default value of Internal or you will find that all of your validation attributes will be silently ignored (including System.ComponentModel.DisplayNameAttribute, interestingly enough).

[DisplayName("Email")]
[Required(ErrorMessageResourceName = "EmailMissing", ErrorMessageResourceType = typeof(GeneralForms))]
public string Email { get; set; }
resourceaccessmodifier

Wednesday, February 17, 2010

ASP.NET MVC2 RC2 - Breaking Area Registration Change

With the release of ASP.NET MVC RC2 there is a breaking change in the way area registration works. As of RC2, all AreaRegistration types must be marked public, or they will be ignored by calls to AreaRegistration.RegisterAllAreas(). To me this seems rather arbitrary in nature, but I suspect the reasoning for the change stems from the fact that with the initial preview of MVC2 areas had to be implemented as separate projects. Thus, if the access modifier on the AreaRegistration type for a given Area project is not public, it would not be picked up by the call to RegisterAllAreas() in the main web project without violating access modifier constraints (private member reflection). So to reiterate, if you were originally doing something like this:
internal class AdminArea : AreaRegistration {}
You must change the access modifier to public in order for the area to be registered and added to the routes table.