Jason Conway

just trying to help
Include and Exclude Constraints in ASP.NET MVC

Otherwise known as a white list or black list for route tokens, this simple IRouteConstraint is coming in handy and I thought i would share:

  1. public enum ListConstraintType {
  2.     Exclude,
  3.     Include
  4. }
  5.  
  6. public class ListConstraint : IRouteConstraint {
  7.     public ListConstraintType ListType { get; set; }
  8.     public IList<string> List { get; set; }
  9.  
  10.     public ListConstraint() : this(ListConstraintType.Include, new string[] { }) { }
  11.     public ListConstraint(params string[] list) : this(ListConstraintType.Include, list) { }
  12.     public ListConstraint(ListConstraintType listType, params string[] list) {
  13.         if (list == null) throw new ArgumentNullException("list");
  14.  
  15.         this.ListType = listType;
  16.         this.List = new List<string>(list);
  17.     }
  18.  
  19.     public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
  20.         if (string.IsNullOrEmpty(parameterName)) throw new ArgumentNullException("parameterName");
  21.         if (values == null) throw new ArgumentNullException("values");
  22.  
  23.         string value = values[parameterName.ToLower()] as string;
  24.         bool found = this.List.Any(s => s.Equals(value, StringComparison.OrdinalIgnoreCase));
  25.  
  26.         return this.ListType == ListConstraintType.Include ? found : !found;
  27.     }
  28. }

You can then use the ListConstraint like this:

  1. public static void RegisterRoutes(RouteCollection routes) {
  2.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  3.     routes.IgnoreRoute("content/{*pathInfo}");
  4.     routes.IgnoreRoute("favicon.ico");
  5.  
  6.     routes.MapRoute(
  7.         "UserShortcuts",
  8.         "{action}/{id}",
  9.         new { controller = "user", id = "" },
  10.         new { action = new ListConstraint(ListConstraintType.Include, "signin", "signout") }
  11.     );
  12.  
  13.     routes.MapRoute(
  14.         "HomeShortcuts",
  15.         "{action}",
  16.         new { controller = "home" },
  17.         new { action = new ListConstraint("sitemap", "contact") } //default Include
  18.     );
  19.  
  20.     routes.MapRoute(
  21.         "Default",
  22.         "{controller}/{action}/{id}",
  23.         new { controller = "home", action = "index", id = "" },
  24.         new { id = new ListConstraint(ListConstraintType.Exclude, "123") }
  25.     );
  26. }
  27.  
  28. protected void Application_Start() {
  29.     RegisterRoutes(RouteTable.Routes);
  30. }

Which then allows only those included constraints to use the UserShortcuts route:

Or prevents the id of 123 from being used on the Default route:

 

Hope folks find this useful,

Jason Conway

Aliasing and Localizing Routes in ASP.NET MVC

I had come across a forum post where someone wanted to localize urls in the RouteTable and I thought I would come up with my first stab at a better solution.  What I discovered turned out to be a pretty simple way of what I am calling ‘Aliasing Routes’ where a route token can have an alias value as a localized value.

DOWNLOAD – My entire MvcSamples VSTS solution (complete with Moq and 100% code coverage)

This is how you do it (the complete source is available for download above):

Step 1: Persistence – we need a way to store the aliases, so I created a custom ConfigurationSection (an external file or database would also work) that looks like this when complete:

  1. <aliasRoutes>
  2.   <routes>
  3.     <add token="controller" value="home" aliases="inicio, beginning, baile, start" />
  4.     <add token="controller" value="about" aliases="info, sobre, more, additional" />
  5.     <add token="action" value="signin" aliases="login, logon, ingresar, anmelden" />
  6.   </routes>
  7. </aliasRoutes>

Step 2: Module – we need a way to inspect route data and change route values when applicable, so I derived from UrlRoutingModule that looks like this when complete:

  1. public class AliasRoutingModule : UrlRoutingModule {
  2.     protected static readonly object itemsKey = new object();
  3.  
  4.     private class RequestInfo {
  5.         public string OriginalPath { get; set; }
  6.         public IHttpHandler HttpHandler { get; set; }
  7.     }
  8.  
  9.     public override void PostMapRequestHandler(HttpContextBase context) {
  10.         Guard.AgainstNullParameter(context, "context");
  11.  
  12.         RequestInfo info = context.Items[itemsKey] as RequestInfo;
  13.         if (info == null) return;
  14.  
  15.         context.RewritePath(info.OriginalPath);
  16.         context.Handler = info.HttpHandler;
  17.     }
  18.  
  19.     public override void PostResolveRequestCache(HttpContextBase context) {
  20.         Guard.AgainstNullParameter(context, "context");
  21.  
  22.         RouteData routeData = this.RouteCollection.GetRouteData(context);
  23.         if (routeData == null) return;
  24.  
  25.         IRouteHandler routeHandler = routeData.RouteHandler;
  26.         if (routeHandler == null) throw new InvalidOperationException("No RouteHandler identified");
  27.         if (routeHandler is StopRoutingHandler) return;
  28.  
  29.         IDictionary<string, object> values = new Dictionary<string, object>(routeData.Values);
  30.         AliasRoutesSection section = SectionRepository.Current.AliasRoutes;
  31.  
  32.         //*** magic happens here
  33.         foreach (string token in values.Keys) {
  34.             string alias = values[token].ToString();
  35.             string value = section.Routes.GetValue(token, alias);
  36.  
  37.             if (value != null) routeData.Values[token] = value;
  38.         }
  39.  
  40.         RequestContext requestContext = new RequestContext(context, routeData);
  41.         IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
  42.         if (httpHandler == null) throw new InvalidOperationException("No HttpHandler identified");
  43.  
  44.         RequestInfo info = new RequestInfo {
  45.             OriginalPath = context.Request.Path,
  46.             HttpHandler = httpHandler
  47.         };
  48.  
  49.         context.Items[itemsKey] = info;
  50.         context.RewritePath("~/UrlRouting.axd");
  51.     }
  52. }

Step 3: Web Config – need to change our UrlRoutingModule registration in the web.config to the new AliasRoutingModule:

  1.   <httpModules>
  2.     <add name="ScriptModule"
  3.          type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
  4.     <add name="AliasRoutingModule"
  5.          type="Salient6.MvcSamples.Web.Modules.AliasRoutingModule"/>
  6.   </httpModules>
  7. </system.web>

Step 4: RegisterRoutes – our Global stays clean and manageable:

  1. public static void RegisterRoutes(RouteCollection routes) {
  2.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
  3.     routes.IgnoreRoute("content/{*pathInfo}");
  4.     routes.IgnoreRoute("favicon.ico");
  5.  
  6.     routes.MapRoute(
  7.         "Default",                                              
  8.         "{controller}/{action}/{id}",                           
  9.         new { controller = "home", action = "index", id = "" }
  10.     );
  11. }

Step 5: Browser – we should be able to get to the Home Controller now by navigating to /start:

Alias Route Start

OR something way cooler like /inicio/anmelden  which is a combination of Controller and Action aliases!

Alias Route Start

What’s nice about this is you can alias and/or localize your url without having to MapRoute, you can make changes in the config without having to recompile, and you can mix and match aliases as long as they resolve to a valid endpoint!

Anyway, I hope folks find this useful!

Jason Conway

kick it on DotNetKicks.com

Using the ConwayControls RadioButton - Scenario 2

Scenario

Upon a simple asp:Button click event (postback), iterate the rows of a GridView finding the checked RadioButton

 

ASP.NET - RadioButtonSpike2.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="RadioButtonSpike2.aspx.cs" 
      Inherits="RadioButtonSpike2" %>

<%@ Register Assembly="ConwayControls" Namespace="ConwayControls.Web" TagPrefix="ccwc" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Radio Button Spike - Scenario 2</title>
        <style type="text/css" media="all">
            div {padding-top: 10px;}
            h3 {display: inline;}
        </style>        
    </head>
    <body>
        <form id="RadioButtonSpikeForm" runat="server">
            <div>
                <asp:GridView ID="SurveyGrid" runat="server" AutoGenerateColumns="False">
                    <Columns>
                        <asp:TemplateField HeaderText="Language">
                            <ItemTemplate>
                                <asp:Label ID="NameLabel" runat="server" Text='<%# Eval("Name") %>' />
                            </ItemTemplate>
                        </asp:TemplateField>
                        <asp:TemplateField HeaderText="Favorite">
                            <ItemTemplate>
                                <ccwc:RadioButton ID="FavoriteButton" runat="server" GroupName="FavoriteGroup" 
                                    Value='<%# Eval("Id") %>' />
                            </ItemTemplate>
                        </asp:TemplateField>
                    </Columns>
                </asp:GridView>  
                
                <div>
                    <asp:Button ID="ForPostBackButton" runat="server" Text="for post back" 
                        OnClick="ForPostBackButton_Click" />
                </div>
                
                <div>
                    <asp:Label ID="ResultsLabel" runat="server" />
                </div>                          
            </div>
        </form>
    </body>
</html>

C# (code behind) - RadioButtonSpike2.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using CCW = ConwayControls.Web;

public partial class RadioButtonSpike2 : Page {

    protected override void OnLoad(EventArgs e) {
        base.OnLoad(e);

        if (this.IsPostBack) return;

        this.BindGrid();
    }

    protected void BindGrid() {
        List<SurveyItem> items = new List<SurveyItem>();

        items.Add(new SurveyItem(1, "C#"));
        items.Add(new SurveyItem(2, "VB"));
        items.Add(new SurveyItem(3, "Perl"));
        items.Add(new SurveyItem(4, "Java"));
        items.Add(new SurveyItem(5, "Ruby"));

        this.SurveyGrid.DataSource = items;
        this.SurveyGrid.DataBind();
    }

    protected void ForPostBackButton_Click(object sender, EventArgs e) {
        foreach (GridViewRow row in this.SurveyGrid.Rows) {
            CCW.RadioButton radioButton = row.FindControl("FavoriteButton") as CCW.RadioButton;

            if (radioButton == null || !radioButton.Checked) continue;

            //the following label demonstrates the ability of grabbing other controls in the row and using it...
            Label label = row.FindControl("NameLabel") as Label;

            if (label != null) {
                string format = "favorite language: <h3>{0} (Id: {1})</h3>";
                this.ResultsLabel.Text = string.Format(format, label.Text, radioButton.Value);
                break;
            }
        }
    }
}

public class SurveyItem {
    public SurveyItem(int id, string name) {
        this.id = id;
        this.name = name;
    }

    public int Id {
        get { return this.id; }
        set { this.id = value; }
    }    private int id;

    public string Name {
        get { return this.name; }
        set { this.name = value; }
    }    private string name;

}

 

It is a good practice when iterating over a collection to continue after an unwanted test and break once you have found the item you were looking for.

As noted in the iteration of the GridViewRows inside the ForPostBackButton_Click method, I tested to ensure the RadioButton was found, that it was checked, and continued if it wasn't by using the continue keyword. Once the checked RadioButton was found, I broke the iteration using the break keyword. Both statements/uses prevent any unnecessary code execution which is even more important during iteration of a collection.

Please let me know if you have any questions with this particular scenario.

Thanks,

Jason Conway

Meta Tags in ASP.NET using MasterPage and ContentPlaceHolders

Having META tags like Description and Keywords on the .aspx pages of our sites is very important.  It is also important that the content of the tags be easily added and/or updated.  Most online help will have you instantiate an HtmlMeta class or write tags by implementing some variance of either:

  • Option 1 - create a base Page class with methods or properties and create an external persistence store for META content
  • Option 2 - create a base Page class with virtual methods or properties and override those on the deriving page
  • Option 3 - create an Interface or abstract class with methods or properties that the deriving page will employ
  • Option 4 - use a third-party component or write a control
  • Option 5 - on every page, write or use existing implementation to create META tags

While my solution also uses the HtmlMeta class, it differs in architecture and uses the following guidelines:

  • create META tags on pages in a new or existing Site
  • no base Page class, abstract class, or Interface that all existing or new pages inherit
  • no database or external store for persisting Tag content
  • no controls or third-party components
  • just want META content and not the actual tag creation on every page
  • must provide unique content per page
  • must provide simple, maintainable content changes without recompile
  • must provide the ability to change content with basic skills and without a special interface

In other words, I did not want to create a base Page, Interface, control, or third-party component that every single one of the pages in my site derives from or uses.  In addition having to ensure that any newly created pages inherited the base Page or implement some Interface is not as easily maintainable.  I can also use this solution on existing sites without having to worry about any Page inheritance issues.

What better place to keep information about an .aspx file than right in the .aspx file?  Most markup resides in each .aspx file using ContentPlaceHolders anyway and when you create a new page, these ContentPlaceHolders will be included automatically!  For this post, I am only going to demonstrate adding META Description and Keywords tags, but this process can be applied to as many tags as you need.  One thing that is great about this approach, is that you will not need a database or external file (like xml) to store META content...making the content very easily updated with a simple text editor (like Notepad) and no additional programming or database skills!

DOWNLOAD - Meta Spike Files

This is how you do it (the source files are available for download above):

  1. drop 2 ContentPlaceHolders in a MasterPage's head tag, 1 for Description and 1 for Keywords making sure to set  Visible="false" 
  2. wire up Load events on the ContentPlaceHolders and LoadComplete on the MasterPage's Page property
  3. implement handled events
  4. create or update existing page with new Description and Keywords ContentPlaceHolders add content to place holders
  5. view page in browser and inspect rendered source

Step 1 - default.master

<%@ Master Language="C#" AutoEventWireup="true" CodeBehind="Default.master.cs" Inherits="MetaSpike.Default" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title></title>
       <asp:ContentPlaceHolder id="MetaDescriptionHolder" runat="server" Visible="false"></asp:ContentPlaceHolder>
        <asp:ContentPlaceHolder ID="MetaKeywordsHolder" runat="server" Visible="false"></asp:ContentPlaceHolder>
    </head>
    <body>
        <form id="DefaultMasterForm" runat="server">
            <div>
                <asp:ContentPlaceHolder ID="MainContentHolder" runat="server"></asp:ContentPlaceHolder>
            </div>
        </form>
    </body>
</html>

Step 2 - default.master.cs

using System;
using System.Web.UI;

namespace MetaSpike {
    public partial class Default : MasterPage {

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            this.MetaDescriptionHolder.Load += new EventHandler(MetaDescriptionHolder_Load);
            this.MetaKeywordsHolder.Load += new EventHandler(MetaKeywordsHolder_Load);

            this.Page.LoadComplete += new EventHandler(Page_LoadComplete);
        }

        private void MetaDescriptionHolder_Load(object sender, EventArgs e) {
            throw new NotImplementedException();
        }

        private void MetaKeywordsHolder_Load(object sender, EventArgs e) {
            throw new NotImplementedException();
        }

        private void Page_LoadComplete(object sender, EventArgs e) {
            throw new NotImplementedException();
        }
    }
}

Step 3 - default.master.cs (continued)

using System;
using System.Web.UI;
using System.Web.UI.HtmlControls;
using System.Web.UI.WebControls;

namespace MetaSpike {
    public partial class Default : MasterPage {
        private HtmlMeta descriptionMeta;
        private HtmlMeta keywordsMeta;

        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            this.MetaDescriptionHolder.Load += new EventHandler(MetaDescriptionHolder_Load);
            this.MetaKeywordsHolder.Load += new EventHandler(MetaKeywordsHolder_Load);

            this.Page.LoadComplete += new EventHandler(Page_LoadComplete);
        }

        private void MetaDescriptionHolder_Load(object sender, EventArgs e) {
            string content = this.ParseHolderContent(this.MetaDescriptionHolder);

            if (string.IsNullOrEmpty(content)) return;

            this.descriptionMeta = new HtmlMeta();
            this.descriptionMeta.Name = "description";
            this.descriptionMeta.Content = content;
        }

        private void MetaKeywordsHolder_Load(object sender, EventArgs e) {
            string content = this.ParseHolderContent(this.MetaKeywordsHolder);

            if (string.IsNullOrEmpty(content)) return;

            this.keywordsMeta = new HtmlMeta();
            this.keywordsMeta.Name = "keywords";
            this.keywordsMeta.Content = content;
        }

        private void Page_LoadComplete(object sender, EventArgs e) {
            Page page = sender as Page;
            if (page == null) return;

            if (this.descriptionMeta != null) page.Header.Controls.Add(this.descriptionMeta);
            if (this.keywordsMeta != null) page.Header.Controls.Add(this.keywordsMeta);
        }

        private string ParseHolderContent(ContentPlaceHolder holder) {
            if (holder == null || holder.Controls.Count == 0) return string.Empty;

            LiteralControl control = holder.Controls[0] as LiteralControl;
            if (control == null || string.IsNullOrEmpty(control.Text)) return string.Empty;

            return control.Text.Trim();
        }
    }
}

Step 4 - home.aspx

<%@ Page Language="C#" MasterPageFile="~/Default.Master" AutoEventWireup="true" 
    CodeBehind="Home.aspx.cs" Inherits="MetaSpike.Home" Title="Meta Spike Home" %>
    
<asp:Content ID="MetaDescription" ContentPlaceHolderID="MetaDescriptionHolder" runat="server">description goes here</asp:Content>
<asp:Content ID="MetaKeywords" ContentPlaceHolderID="MetaKeywordsHolder" runat="server">keywords, .net, asp.net, meta</asp:Content>

<asp:Content ID="MainContent" ContentPlaceHolderID="MainContentHolder" runat="server">
    this page now has a very simple mechanism for adding and updating meta tags! enjoy!
</asp:Content>

Step 5 - view source rendered home.aspx

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" >
    <head>
        <title>Meta Spike Home</title>
        <meta name="description" content="description goes here" />
        <meta name="keywords" content="keywords, .net, asp.net, meta" />
    </head>
    <body>
        <form name="aspnetForm" method="post" action="Home.aspx" id="aspnetForm">
            <div>
                <input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
 		   value="/wEPDwUJNDMyNDU0NjAzZGQfTG4D56NhuIUPL9dxPlf1j85RXw==" />
            </div>
            <div>              
                this page now has a very simple mechanism for adding and updating meta tags! enjoy!
            </div>
        </form>
    </body>
</html>

Some things to note:

  • ContentPlaceHolders on the MasterPage have Visible set to false
  • the use of the Page's LoadComplete event instead of Load
  • only straight text in the Content tags on Home.aspx, ASP.NET will put that text into LiteralControls
  • no text in the Content tags on Home.aspx will result in no META tags being output (good thing)

This will prevent you from having to write more code than you need and leaves the actual content on the page itself (where it belongs).  In addition, you won't have a database or special component to worry about or pay for.

I hope you find this approach very simple, maintainable, and easily employed.

Jason Conway

kick it on DotNetKicks.com

Using the ConwayControls RadioButton - Scenario 1

Scenario

Upon a simple asp:Button click event (postback), capture the CheckChanged event of the RadioButton

ASP.NET - RadioButtonSpike1.aspx

<%@ Page Language="C#" AutoEventWireup="true" CodeFile="RadioButtonSpike1.aspx.cs" Inherits="RadioButtonSpike1" %>

<%@ Register Assembly="ConwayControls" Namespace="ConwayControls.Web" TagPrefix="ccwc" %>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
    <head runat="server">
        <title>Radio Button Spike - Scenario 1</title>
        <style type="text/css" media="all">
            div {padding-top: 10px;}
            h3 {display: inline;}
        </style>
    </head>
    <body>
        <form id="RadioButtonSpikeForm" runat="server">
            <div>
                <asp:GridView ID="SurveyGrid" runat="server" AutoGenerateColumns="False">
                    <Columns>
                        <asp:TemplateField HeaderText="Language">
                            <ItemTemplate>
                                <asp:Label ID="NameLabel" runat="server" Text='<%# Eval("Name") %>'></asp:Label>
                            </ItemTemplate>
                        </asp:TemplateField>
                        <asp:TemplateField HeaderText="Favorite">
                            <ItemTemplate>
                                <ccwc:RadioButton ID="FavoriteButton" runat="server" GroupName="FavoriteGroup" 
                                    OnCheckChanged="FavoriteButton_CheckChanged" Value='<%# Eval("Name") %>' />
                            </ItemTemplate>
                        </asp:TemplateField>
                    </Columns>
                </asp:GridView>
                
                <div>
                    <asp:Button ID="ForPostBackButton" runat="server" OnClick="ForPostBackButton_Click" Text="for post back" />
                </div>
                
                <div>
                    <asp:Label ID="ResultsLabel" runat="server"></asp:Label>
                </div>
            </div>
        </form>
    </body>
</html>

C# (code behind) - RadioButtonSpike1.aspx.cs

using System;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Collections.Generic;
using CCW = ConwayControls.Web;

public partial class RadioButtonSpike1 : Page {
    protected override void OnLoad(EventArgs e) {
        base.OnLoad(e);

        if (this.IsPostBack) return;

        this.BindGrid();
    }

    protected void BindGrid() {
        List<SurveyItem> items = new List<SurveyItem>();

        items.Add(new SurveyItem(1, "C#"));
        items.Add(new SurveyItem(2, "VB"));
        items.Add(new SurveyItem(3, "Perl"));
        items.Add(new SurveyItem(4, "Java"));
        items.Add(new SurveyItem(5, "Ruby"));

        this.SurveyGrid.DataSource = items;
        this.SurveyGrid.DataBind();
    }

    protected void ForPostBackButton_Click(object sender, EventArgs e) {
        //do nothing...just needed the postback
    }

    protected void FavoriteButton_CheckChanged(object sender, EventArgs e) {
        CCW.RadioButton radioButton = sender as CCW.RadioButton;

        if (radioButton != null && radioButton.Checked) {
            this.ResultsLabel.Text = string.Format("your favorite language is: <h3>{0}</h3>", radioButton.Value);
        }
    }
}

public class SurveyItem {
    public SurveyItem(int id, string name) {
        this.id = id;
        this.name = name;
    }

    public int Id {
        get { return this.id; }
        set { this.id = value; }
    }    private int id;

    public string Name {
        get { return this.name; }
        set { this.name = value; }
    }    private string name;

}

 

Now keep in mind that in this scenario and implementation, the CheckChanged event will fire if the RadioButton changes from being checked or unchecked on any postback.  That means that if your grid has paging or a different unrelated button that posts, you are not going to want to ignore this event when it happens. 

You will still use this implementation for grids that page or unrelated postbacks; you will just need to keep track of changes by using some sort of state mechanism.  The other ways to use the RadioButton are:

  • enable AutoPostBack
  • iterate over all the rows in the grid (sledge hammer approach)

I will demonstrate these other scenarios, including a grid with paging, in posts to follow.

Please let me know if you have any questions with this particular scenario.

Thanks,

Jason Conway

RadioButton for the DataGrid or GridView

ConwayControls.zip

The .zip file above contains a custom RadioButton control that works inside a DataGrid or GridView.

Can be used for both 1.1 or 2.0!

Screen Shot of the RadioButton

More Posts