Tuesday, October 23, 2012

ASP.NET MVC – Get field Name and Id for Model properties

ASP.NET MVC provides a number of convenient HtmlHelpers for form elements which accept a Lamda Expression returning a Model property as their first argument. The key advantages of these HtmlHelpers is that they simplify value binding and guarantee compile-time type-safety for selected Model members.

Html.TextBoxFor(m => m.Request.Name)
Html.LabelFor(m => m.Request.Name)

Sometimes it is necessary to know what the generated field Name or Id will be elsewhere in the view, but unfortunately, there is no public method available to return these values.

The following HtmlHelpers accept a Model property expression and return the same field Id and Name strings which would be produced by calling the standard form helpers.

public static string GetFullHtmlFieldName<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
    return helper.ViewData.TemplateInfo.GetFullHtmlFieldName(expression);
}

public static string GetFullHtmlFieldId<TModel, TProperty>(this HtmlHelper<TModel> helper, Expression<Func<TModel, TProperty>> expression)
{
    return helper.ViewData.TemplateInfo.GetFullHtmlFieldId(expression);
}

public static string GetFullHtmlFieldName<TModel, TProperty>(this TemplateInfo templateInfo, Expression<Func<TModel, TProperty>> expression)
{
    return templateInfo.GetFullHtmlFieldName(ExpressionHelper.GetExpressionText(expression));
}

public static string GetFullHtmlFieldId<TModel, TProperty>(this TemplateInfo templateInfo, Expression<Func<TModel, TProperty>> expression)
{
    return templateInfo.GetFullHtmlFieldId(ExpressionHelper.GetExpressionText(expression));
}

Thursday, October 11, 2012

Identifying browsers with JavaScript disabled

In this day and age, a user without JavaScript enabled is going to have an awful time on the web and, quite frankly, “graceful degradation” and similar no-JavaScript fallbacks are a waste of time. That being said, sometimes it’s still important to be able to identify visitors who choose to navigate your website without any JavaScript.

Active Approach

A common technique for detecting whether a user has JavaScript enabled is to use JavaScript to set a form value and then check that value on the server after submission. While this is a valid approach, it’s not always ideal because it requires that some kind of interaction takes place on the client.

<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script type="text/javascript">
    $(function() {
        $('#jsEnabled').val('true');
    });
</script>

<form>
    <input type="hidden" id="jsEnabled" name="jsEnabled" value="false" />
    <input type="submit" />
</form>

Passive Approach

Another technique, and the one we use at ScoreBig.com, is to leverage the <noscript /> tag. <noscript /> identifies any block of code which should be rendered when JavaScript is not enabled and it is supported by all major browsers. By placing a request to an image resource on our server in a <noscript /> block we can infer that any a request made to that resource is with a browser with JavaScript disabled. The best part about this approach is that it requires no action by the user; the browser does all of the work.

<noscript>
    <img src="__nojs.gif" width="1" height="1" style="display: none" />
</noscript>

Monday, May 7, 2012

C# Truncate String - Whole Words

This is an update to my original string truncate helper which can be set to respect whitespace when terminating a string. This version will start at the character index specified as max length and then iterate back to the first instance of whitespace (if it exists), thus keeping whole words intact.

public static string Truncate(this string str, int maxLength, bool onlyOnWhiteSpace = false, bool appendEllipses = false) {
    if (str == null || maxLength >= str.Length)
    {
        return str;
    }

    if (onlyOnWhiteSpace
        && !char.IsWhiteSpace(str[maxLength])
        && str.Substring(0, maxLength).Any(char.IsWhiteSpace))
    {
        for (; maxLength >= 0; maxLength--)
        {
            if (char.IsWhiteSpace(str[maxLength]))
            {
                break;
            }
        }
    }

    return str.Substring(0, maxLength) + (appendEllipses ? "..." : "");
}

Tuesday, November 22, 2011

ASP.NET MVC Comma Separated Values Model Binder

In some scenarios such as data filtering it becomes necessary to pass multiple values to a single collection-typed request parameter. In typical flows this is achieved by passing multiple pairs of key and value parameters to the request.

/path/to/foo?key=value1&key=value2&key=value3

While this works well, sometimes it is desirable to have something that is more human-readable. In an effort to satisfy that requirement, I have built a custom model binder to handle passing multiple values to a single parameter in the form of a single comma separated value.

/path/to/foo?key=value1,value2,value3

This CommaSeparatedValuesModelBinder supports both generic collections and standard array types so long as the underlying type of the collection inherits from IConvertible (int, string, etc). In addition to supporting a comma separated value assignment, the implementation also maintains support for the default format of multiple key and value pairs.

Here's the code:

public class CommaSeparatedValuesModelBinder : DefaultModelBinder
{
    private static readonly MethodInfo ToArrayMethod = typeof(Enumerable).GetMethod("ToArray");

    protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, 
        PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder)
    {
        if (propertyDescriptor.PropertyType.GetInterface(typeof (IEnumerable).Name) != null)
        {
            var actualValue = bindingContext.ValueProvider.GetValue(propertyDescriptor.Name);

            if (actualValue != null && !String.IsNullOrWhiteSpace(actualValue.AttemptedValue) &&
                actualValue.AttemptedValue.Contains(","))
            {
                var valueType = propertyDescriptor.PropertyType.GetElementType() ??
                                propertyDescriptor.PropertyType.GetGenericArguments().FirstOrDefault();

                if (valueType != null && valueType.GetInterface(typeof (IConvertible).Name) != null)
                {
                    var list = (IList) Activator.CreateInstance(typeof (List<>).MakeGenericType(valueType));

                    foreach (var splitValue in actualValue.AttemptedValue.Split(new[] {','}))
                    {
                        if(valueType.IsEnum)
                        {
                            try
                            {
                                list.Add(Enum.Parse(valueType, splitValue));
                            }
                            catch { }
                        }
                        else
                        {
                            list.Add(Convert.ChangeType(splitValue, valueType));
                        }
                    }

                    if (propertyDescriptor.PropertyType.IsArray)
                    {
                        return ToArrayMethod.MakeGenericMethod(valueType).Invoke(this, new[] {list});
                    }
                    return list;
                }
            }
        }

        return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder);
    }
}

Thursday, January 13, 2011

How to Get the Fully Qualified Name of a .NET Assembly

In .NET, when referencing satellite assemblies it is generally best practice to use an assembly’s fully qualified name, which includes the name, version number, culture and public key token.

The easiest way I have found to determine the fully qualified name of an assembly is to simply drop it into Red Gate’s .NET Reflector, select it, and check the information displayed at the bottom of the window.

Besides the assembly name, the rest of the elements in a fully qualified name are optional, but including them can help to secure your applications.

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.