ASP.NET MVC: Displaying and Retrieving Values from a List of Checkboxes


I ran into a snag the other day when trying to display a list of checkboxes and then retrieve the values of the ones the user checked using ASP.NET MVC. After some research it turns out it is pretty simple to do. Imagine that you are giving a user the ability to add a product to a catalog and you want to display a list of checkboxes that will allow the user to associate the product with one or more categories. Something to the effect of that shown below.

First, we will pass the view a list of ProductCategory objects.

[HttpGet]
public ActionResult New()
{
    // Create a list of product categories manually for example purposes
    List<ProductCategory> categories = new List<ProductCategory>()
    {
        new ProductCategory() { Id = 1, Name = "Books" },
        new ProductCategory() { Id = 2, Name = "Games" },
        new ProductCategory() { Id = 3, Name = "Videos" },
        new ProductCategory() { Id = 4, Name = "Music" }
    };

    ViewData["categories"] = categories;

    return View(new ProductsViewModel());
}

In the View, we then need to loop through the ProductCategory objects and create a new checkbox for each category. The first key to making this work is to ensure that each of the checkboxes have the same name. Below I have named each of the checkboxes CategoryIds. The second key is to assign the value attribute a value that identifies each of the categories, in this case, the Id property of the category. Include the following code in the View where you want to display the checkboxes.

<% foreach (var category in (List<MVCTest.Models.ProductCategory>)ViewData["categories"]) { %>
    <input type="checkbox" name="CategoryIds" value="<%: category.Id %>" /> <%: category.Name %>
<% } %>

Using the model binding magic of ASP.NET MVC we can create an array of integers named CategoryIds in the model that is returned from the View and it will be automatically be populated with the values assigned to the value attribute of the each checkboxes we created for each product category. Just make sure that the name of the array matches the name of each of the checkboxes you created. Below is an example of the model returned from the View.

public class ProductsViewModel
{
    public string Title { get; set; }
    public string Description { get; set; }
    public decimal Price { get; set; }
 
    // Integer array with name matching that of the checkboxes it's associated with
    public int[] CategoryIds { get; set; }
}

Then in the action method that is called on the post, add the ProductViewModel as a parameter. As mentioned, the ProductsViewModel.CategoryIds array will automatically be populated with the ids of the categories that the user checked.

[HttpPost]
public ActionResult New(ProductsViewModel product)
{
       // The prodcut.CategoryIds array will be populated
       // with the values from the checked checkboxes
}

In the example above, the ProductViewModel.CategoryIds array will have a single element, 2, since we only checked the Games category.

Determine Which Version of Windows Installer is Installed


When deploying an application to a client, it may be necessary to upgrade the installed version of Windows Installer. For example, SQL Server 2008 requires that Windows Installer 4.5 be installed on the system, something that most Windows XP machines won’t have. It is simplest to just package the needed version of Windows Installer with your application, but if you need to check which version is installed manually, you can do one of the following.

Msi.dll Version

  1. Navigate to the Windows\System32 directory of the computer (Generally C:\Windows\System32)
  2. Find and check the version number of the file msi.dll

Running msiexec

  1. Click Start
  2. Click Run
  3. Type msiexec
  4. Press enter
  5. In the window that appears, the version of Windows Installer will be on the first line

ASP.NET MVC: Displaying a PDF Document in the Browser


Returning a pdf document (or any file for that matter) is very simple using ASP.NET MVC due to the fact that action methods can return a result of type FileResult or FilePathResult. To return a file stored in the App_Data directory, simply do the following:

public FileResult GetFile(string fileName)
{
    string path = AppDomain.CurrentDomain.BaseDirectory + "App_Data/";            
    return File(path + fileName, System.Net.Mime.MediaTypeNames.Application.Pdf, fileName);
}

But, despite the fact that we have indicated that the MIME type is ‘application/pdf’, the user will be prompted with a download dialog box instead of the pdf simply being displayed in the browser. This is a shortcoming in the ASP.NET MVC framework that can be fixed by adding a line to the header of the response. To make the browser display the pdf file, add the following line of code to your action method:

public FileResult GetFile(string fileName)
{
    // Force the pdf document to be displayed in the browser
    Response.AppendHeader("Content-Disposition", "inline; filename=" + fileName + ";");

    string path = AppDomain.CurrentDomain.BaseDirectory + "App_Data/";            
    return File(path + fileName, System.Net.Mime.MediaTypeNames.Application.Pdf, fileName);
}

Adding the value ‘inilne’ to the Content-Disposition attribute indicates to the browser that it should be displayed immediately rather than downloaded by the user.

ASP.NET MVC: Custom Constraints – Matching the User Agent String


I have been really diving into routing with ASP.NET MVC and inevitably I came to the topic of custom constraints on routes. One of the really nice applications of custom constraints is matching a string contained within the client’s user agent string sent along with the request from the browser. For those that don’t know, the user agent is a string that is sent from the browser with information such as the type of browser used to make the request, the operating system of the client, and for most mobile phone browsers, the type of phone making the request. Check out useragentstring.com to see a detailed explanation of your browser’s user agent string.

If you have a web application that should render differently on a mobile phone browser, say the iPhone, you can check the user agent string to see if the request is coming from an iPhone and if it is, you can direct it accordingly. To do such a thing in ASP.NET MVC, first we need to create a UserAgentConstraint class implementing the IRouteConstraint interface that will check to see if a given string is found within the client’s user agent string.

public class UserAgentConstraint : IRouteConstraint
{
    public string _stringInUserAgent;
    public bool _caseSensitive;

    public UserAgentConstraint(string stringInUserAgent, bool caseSensitive)
    {
        this._stringInUserAgent = stringInUserAgent;
        this._caseSensitive = caseSensitive;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (httpContext.Request.UserAgent == null)
            return false;

        // Check to see if the indicate string is found within the client's user agent string
        if (_caseSensitive)
            return httpContext.Request.UserAgent.Contains(_stringInUserAgent);

        // If not case sensitve, make everything lowercase and then check    
        return httpContext.Request.UserAgent.ToLower().Contains(_stringInUserAgent.ToLower());
    }
}

When a constraint is checked, the Match method is called and returns whether or not the constraint is satisfied.

Now when defining the route, simply set the constraint for the user agent as a new instance of the UserAgentConstraint class matching the word iphone.

public static void RegisterRoutes(RouteCollection routes)
{
    ...

    routes.MapRoute(
        null, // Route name
        "Home", // URL to match
        new { controller = "Home", action = "IndexIPhone" }, // Parameter defaults
        new { userAgent = new UserAgentConstraint("iphone", false) }    // Constraints
    );
}

The above route, a rather dull example, simply directs the request to an action method in the Home controller named IndexIPhone where presumably the same logic resides for non-iPhone requests. In reality, you would probably want to separate these controllers into separate namespaces. Fortunately, you can also specify another RouteValueDictionary that lets the application know which namespace to look in when searching for the controller, but we will leave that for another time.

ASP.NET MVC: Register a Route with a Parameter Constraint and a Default Value Defined in the Action Method


This weekend I was playing around with the routing configuration for an ASP.NET MVC project and ran into an interesting situation. I created a simple test project with a ProductsController containing and action method named List and as well its corresponding view. The List action method accepted an integer parameter named page that would indicate which page of the catalog to display.

I wanted to setup the routes so that the url ~/Catalog (not specifying any page parameter) would be mapped to the List action method so that the first page of the catalog would be displayed. Conversely, if the url ~/Catalog/2 was used, the List action method would be called and the second page would be displayed. This is actually a pretty simple task; all you need to do is set the page parameter in the route as optional as follows:

routes.MapRoute(null, 	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List", page = UrlParameter.Optional } 	// Default values
                );

This simple ASP.NET MVC 101 route works great but the one issue is that this route will match the url ~/Catalog/abc as well and attempt to cast the string ‘abc’ to an integer and cause the application to fail. My first thought was to simply put a constraint on the page parameter as follows to indicate that only digits could be accepted.

routes.MapRoute(null, 	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List", page = UrlParameter.Optional }, 	// Default values
                new { page = @"\d+" } 	// Parameter constraints
                );

This solved the above mentioned problem so that the url ~/Catalog/abc was no longer handled by this route. But, this created another problem, since I put the constraint on the page parameter that it had to be a digit value, this route no longer matched the url ~/Catalog. The simplest solution to this is replace the UrlParameter.Optional value assigned to the page parameter in the default values collection with the value 1 or whatever your default value is. If you do this and navigate to the url ~/Catalog, the ASP.NET MVC framework will automatically pass in the default value of 1 you supplied when register the route. But generally I like to specify the default values to my action methods using the [DefaultValue] attribute or the .NET Framework 4 syntax for specifying optional parameters as shown below.

public ActionResult List(int page = 1)
{
     // Method logic
}

Since I am looking at my controller code much more the the code used for registering routes, I am reminded that a default value is being provided to the method if one is not present in the url or the query string. Purely a personal preference, but one that I like to follow.

After much research, I concluded that there is no way to allow the user to enter both ~/Catalog and ~/Catalog/2 and to provide the default value along with the action method in a single route definition. In order to accomplish what I desired, the following two routes needed to be registered:

// Matches ~/Catalog/2
routes.MapRoute(null,	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List" },	// Default values
                new { page = @"\d+" }	// Parameter constraints
                );

// Matches ~/Catalog
routes.MapRoute(null,	// Route name
                "Catalog",	// Url to match
                new { controller = "Products", action = "List" }	// Default values
                );

If anyone has a solution to this using only one route, please let me know.

Follow

Get every new post delivered to your Inbox.

Join 67 other followers