Using Ionics Isapi Rewrite for ASP.NET MVC in IIS 6

If you are running IIS6 and want to deploy an ASP.NET MVC app it is a royal pain in the behind. Pretty URLs (www.mysite.com/prices instead of www.mysite.com/prices.aspx) aren’t supported out of the box. There are several solutions to this problem. First, you can still use MVC with IIS6 out of the box and just append .mvc to all of your files. This approach works, but you lose the nice URLs. Second, you can add a wildcard mapping that processes EVERY resource in a website through the ASP.NET ISAPI dll (images, css files, javascript, everything!) There is a known performance hit with this method and I found it to be quite noticeable when I tried it. Third, you can configure a URL re-writing module that will enable pretty URLs and do so in a well performing manner.

I wanted to go with option 3, which is a bit more work but I feel the best overall solution. Unfortunately, most of the information and “How To” posts I found out on the internet didn’t work, were horribly outdated or just didn’t make sense. So with that in mind I download the latest version of Ionic’s open source Isapi Rewrite module, read through their documentation and then worked out a configuration in IIS 6 that got everything working. Here is a step by step process that you can follow to easily (and freely) get ASP.NET MVC pretty URLs running under IIS 6 using Ionic’s Isapi Rewrite module.

Ionics Isapi Rewrite Installation and Configuration

  1. Download Ionic Isapi Rewrite http://iirf.codeplex.com/
  2. Unzip the contents
  3. Create the following folder c:\inetpub\IonicRewriter (or a location of your choice)
  4. Copy IonicIsapiRewriter-2.0-Release-bin\bin\IIRF.dll (from the contents of the download you unzipped) to c:\inetpub\IonicRewrite
  5. Create a new text document, name it IirfGlobal.ini
  6. Select both files, right click and choose Properties
  7. Select the Security tab
  8. Ensure IIS_WPG has read & execute permissions on both IIRF.dll and IirfGlobal.ini
  9. Edit IirfGlobal.ini and include the following:
    # IsapiRewrite4.ini
    ## Turn off logging, enable if you need to debug routing
    #RewriteLog  c:\_Logs\iirfLog.out
    #RewriteLogLevel 3
    RewriteFilterPriority HIGH
    IterationLimit 1
    RewriteEngine ON
    StatusUrl /iirfStatus
    RewriteRule ^/Default\.aspx /Home.mvc [I,L]
    RewriteRule ^/$ /Home.mvc [I,L]
    RewriteRule ^/([\w]+)$ /$1.mvc [I,L]
    RewriteRule ^/(?!Content|Scripts|App_Data|Images)([\w]*)/(.*) /$1.mvc/$2 [I,L]

    ** The last rule filters out any files in the Content, Scripts, App_Data and Images folder from the URL rewrite. This prevents a .MVC extension from being appended to root folder of the resource being requested (i.e. mysite.com/Content.mvc/site.css) To add another other folders you want to exclude, just add another |FolderName to the regular expression.


  10. Copy the IirfGlobal.ini file to your website directory and rename it to IIRF.ini
  11. Select IIRF.ini, right click and choose Properties
  12. Ensure IIS_WPG has read & execute permissions on IIRF.ini

Internet Information Services (IIS) 6 Configuration

** These configuration instructions are for a single website. If you want to configure the Ionics Isapi Rewrite module for all of IIS 6, perform these steps on the Websites root directory.

  1. Open IIS Manager
  2. Expand the Web Sites node
  3. Right click on the web site you want to configure and select Properties
  4. Select the ISAPI Filters tab
  5. Click the Add button
  6. Enter "Ionic Rewriter" for the filter name
  7. Browse to c:\inetpub\IonicRewriter\IIRF.ddl and select the file (or the location that you placed IIRF.dll into in Step 3)
  8. Click OK
  9. Select the Home Directory tab
  10. Click Configuration
  11. Click the Add... button under "Application extentions"
  12. Click the Browse button and select C:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll for the executable
  13. Enter .mvc for the extension
  14. Limit verbs to: GET,HEAD,POST,DEBUG
  15. UNCHECK "Verify that file exists"
  16. Click OK
  17. Click OK, OK to close the main properties dialog
  18. In IIS Manager, go to Application Pools and Start/Stop the application pool that your MVC application is running under
  19. In IIS Manager –> Web Sites, select your MVC web site and stop/start the web site

IIRF Status and Troubleshooting

  1. On the server, open Internet Explorer and browse to: http://yourmvcwebsiteurl/iirfStatus
  2. You should see the IIRF Status Report if you followed the steps properly
  3. Specifically check the INI file status for both the Global and Site Specific sections, if there are any problems double check your file locations and file permissions as stated above
  4. If IIRF still isn’t working correctly, consult the IIRF v2.0 Operator’s Guide located in the contents of the zip file you downloaded under AdminGuide/Help. There is a section in the chm titled “Verifying and Troubleshooting Installation”
  5. You can also enable logging in the ini files for IIRF and take a look at the IIRF log output to diagnose problems and errors. If there isn’t a log being generated and un-commenting the logging options that means that IIRF isn’t routing properly.

ASP.NET MVC Application Configuration

  1. Modify your Global.asax route code to handle .mvc routes as follows
    routes.MapRoute(
        "Default.mvc",                                          // IIS 6 Ionic Isapi Rewrite support
        "{controller}.mvc/{action}/{id}",                       // URL with parameters
        new { controller = "Home", action = "Index", id = "" }, // Parameter defaults
        new { controller = @"[^\.]*" }                          // Don't look for a controller for non mvc files (ico, images, etc.)
    );

  2. Build & publisher your MVC application
  3. Copy your MVC application to your server, restart the application pool once more and your nice routes should now work

Download

Here is a zip file containing the IIRF.dll, IirfGlobal.ini and IIRF.ini files as well as a sample ASP.NET MVC application with the Global.asax routing setup. It is everything you need and you can just copy/paste these files into your web application using the instructions above.




Aaron Schnieder
http://www.churchofficeonline.com

GridView in ASP.NET MVC

One of the controls that it seems like everyone in the ASP.NET community is most concerned about losing when switching to ASP.NET MVC is the ubiquitous GridView control to render grid based data. Fear not ASP.NET community there is a GREAT alternative that, in my opinion, has a lot more to offer than the GridView in terms of user experience.

I have seen a few different solutions to display grid data in ASP.NET MVC. There is of course a for loop in the View that will render a table (or CSS based layout that mimics a table.) Telerik is putting together a commercially available GridView for MVC, which looks pretty nice but also has a licensing cost associated with it (if you are using it in a commercial application). http://demos.telerik.com/aspnet-mvc Both solutions work, but I don't like the downsides of either, which is where jqGrid comes in. jqGrid is an awesome, free, open source jQuery based grid control.

jqGrid

Application site: http://www.trirand.com/blog/

Demos: http://trirand.com/blog/jqgrid/jqgrid.html

jqGrid is a very fully featured grid that supports loading data client side via AJAX (JSON) calls, paging, sorting, row editing, etc., etc. etc. I believe it can do everything the Telerik control can do and comes with a fully free license for any application.

ASP.NET MVC jqGrid Demo Application

Since I have already used jqGrid in several applications and got everything working pretty smoothly I figured that it would be helpful to make a full demo application available to the community so it can be downloaded and then the pattern easily adapted into ASP.NET MVC applications.

image

JSON data source

I setup the grid to use a controller method that will return a JSON result formatted so that the jqGrid can use it. In the example the call to get the JSON data can be manipulated on the fly and the contents of the grid changed via an AJAX call by filtering the sales data by Date Range.

public ActionResult JsonSalesCollection(DateTime startDate, DateTime endDate,
            string sidx, string sord, int page, int rows)
        {
            SalesLogic logicLayer = new SalesLogic();
            List<Sale> context;
            // If we aren't filtering by date, return this month's contributions
            if (startDate == DateTime.MinValue || endDate == DateTime.MinValue)
                context = logicLayer.GetSales();
            else // Filter by specified date range
                context = logicLayer.GetSalesByDateRange(startDate, endDate);
            // Calculate page index, total pages, etc. for jqGrid to us for paging
            int pageIndex = Convert.ToInt32(page) - 1;
            int pageSize = rows;
            int totalRecords = context.Count();
            int totalPages = (int)Math.Ceiling((float)totalRecords / (float)pageSize);
            // Order the results based on the order passed into the method
            string orderBy = string.Format("{0} {1}", sidx, sord);
            var sales = context.AsQueryable()
                                .OrderBy(orderBy) // Uses System.Linq.Dynamic library for sorting
                                .Skip(pageIndex * pageSize)
                                .Take(pageSize);
            // Format the data for the jqGrid
            var jsonData = new
            {
                total = totalPages,
                page = page,
                records = totalRecords,
                rows = (
                      from s in sales
                      select new
                      {
                          i = s.Id,
                          cell = new string[] {
                            s.Id.ToString(),
                            s.Quantity.ToString(),
                            s.Product,
                            s.Customer,
                            s.Date.ToShortDateString(), 
                            s.Amount.ToString("c")
                        }
                      }).ToArray()
            };
            // Return the result in json
            return Json(jsonData);
        }


Javascript setup



The call to configure the jqGrid when the document ready event is fired is pretty straight forward.



jQuery("#list").jqGrid({
            url: gridDataUrl + '?startDate=' + startDate.toJSONString() + '&endDate=' + endDate.toJSONString(),
            datatype: "json",
            mtype: 'GET',
            colNames: ['Sale Id', 'Quantity', 'Product', 'Customer', 'Date', 'Amount'],
            colModel: [
              { name: 'Id', index: 'Id', width: 50, align: 'left' },
              { name: 'Quantity', index: 'Quantity', width: 100, align: 'left' },
              { name: 'Product', index: 'Product', width: 100, align: 'left' },
              { name: 'Customer', index: 'Customer', width: 100, align: 'left' },
              { name: 'Date', index: 'Date', width: 100, align: 'left' },
              { name: 'Amount', index: 'Amount', width: 100, align: 'right'}],
            rowNum: 20,
            rowList: [10, 20, 30],
            imgpath: gridimgpath,
            height: 'auto',
            width: '700',
            pager: jQuery('#pager'),
            sortname: 'Id',
            viewrecords: true,
            sortorder: "desc",
            caption: "Sales"
        });


Download



Here is the full application for you to download and play with.




I hope this is helpful in your MVC applications.



Aaron



http://www.churchofficeonline.com

ASP.NET MVC Locale User Control (State/Province, Country)

A while back I wrote a locale control for ASP.NET that gave you a nice and easy way to plug in a cascading state/province – country control into your ASP.NET websites. Now that I have moved on to using ASP.NET MVC for my new projects I needed the same control, only this time in MVC. There were aspects of the previous version of the control that I didn’t like and wanted to refactor anyways, specifically the usage of an UpdatePanel (YUCK), reliance on PostBacks, no unit tests and poor separation of concerns for the presentation, logic and persistence layers. I now present to you the brand new ASP.NET MVC version of my Locale control with all of those issues addressed.

LocaleControlScreenShot

Overview of the ASP.NET MVC Locale User Control

  • ASP.NET MVC v1 User Control (Partial View)
  • jQuery handles the change events and retrieves new State/Province lists via a JSON call
  • Separation of concerns between the Presentation Layer, Logic Layer and Persistence Layer
  • Easily customizable data source via the LocaleDao persistence class
  • Unit tests with full dependency injection for all Controller and Logic methods

Basic Locale User Control Usage

Add the user control to your view:

<p>
    <% Html.RenderPartial("LocaleUserControl"); %>
</p>


Locale User Control View:




<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl" %>
<p>
    <label for="StatesProvinces">State / Province: </label>
    <%= Html.DropDownList("StatesProvinces") %>
</p>
<p>
    <label for="Countries">Country: </label>
    <%= Html.DropDownList("Countries") %>
</p>
<script type="text/javascript">
    $(function() {
        var countries = $("#Countries");
        var statesprovinces = $("#StatesProvinces");
        countries.change(function() {
            statesprovinces.find('option').remove();
            $.getJSON('/Base/StatesProvinces', { countryId: countries.val() }, function(data) {
                $(data).each(function() {
                    $("<option value=" + this.Id + ">" + this.Name + "</option>").appendTo(statesprovinces);
                });
            });
        });
    });
</script>






The BaseController class holds the generic methods to support this user control (and other generic shared user controls in your project.)




public class BaseController : Controller
    {
        // Dependency
        protected ILogicFactory _logicFactory;
        public ILogicFactory LogicFactory
        {
            get { return _logicFactory ?? new LogicFactory(); }
            set { _logicFactory = value; }
        }
        /// <summary>
        /// Gets the states provinces select list.
        /// </summary>
        /// <param name="country">The country to filter the states/provinces list by.</param>
        /// <param name="selectedStateProvince">The State/Province to select. Pass null if no default State/Province selected.</param>
        /// <returns></returns>
        public SelectList GetStatesProvincesSelectList(Country country, StateProvince selectedStateProvince)
        {
            // Create the object to return
            SelectList slSps = null;
            if (country != null)
            {
                // Get the collection of states and provinces
                List<StateProvince> statesProvinces = LogicFactory.LocaleLogic.GetStateProvincesByCountry(country.Id);
                
                // Build a selectlist from the statesprovinces collection
                if (statesProvinces != null)
                {
                    slSps = new SelectList(
                    statesProvinces.Select(s => new SelectListItem
                    {
                        Text = s.Name,
                        Value = s.Id.ToString()
                    })
                    , "Value", "Text", selectedStateProvince == null ? string.Empty : selectedStateProvince.Id.ToString());
                }
            }
            return slSps;
        }
        /// <summary>
        /// Gets the countries select list.
        /// </summary>
        /// <param name="selectedCountry">The country to select. Pass null if no default country selected.</param>
        /// <returns></returns>
        public SelectList GetCountriesSelectList(Country selectedCountry)
        {
            // Get the collection of countries, select United States by default
            List<Country> countries = LogicFactory.LocaleLogic.GetCountries();
            // Create the object to return
            SelectList slCountries = null;
            if (countries != null)
            {
                // Build a selectlist from the countries collection
                slCountries = new SelectList(
                countries.Select(c => new SelectListItem
                {
                    Text = c.Name,
                    Value = c.Id.ToString()
                })
                , "Value", "Text", selectedCountry == null ? string.Empty : selectedCountry.Id.ToString());
            }
            return slCountries;
        }
        [AcceptVerbs(HttpVerbs.Get)]
        public ActionResult StatesProvinces(int countryId)
        {
            // Returns JSON collection of stateprovince that are associated
            // with the countryId passed in
            List<StateProvince> sps = LogicFactory.LocaleLogic.GetStateProvincesByCountry(countryId);
            // Format the JSON return
            return Json(sps.Select(s => new
            {
                s.Id,
                s.Name
            }));
        }
    }


Data Source



The data source for the locale control is an XML file that is included in the App_Data folder. You might want to put this data into your database and then use a nice ORM like NHibernate to load the data up in the persistence layer rather than relying on the XML file. This is very easy to accomplish by changing the LocaleDao class, everything else should just work after you swap out the back end.



Unit Tests



I used Microsoft Visual Studio Tests to write unit tests for all of the controller methods and logic layer methods that had any sort of logic in them. I also used Moq to mock the dependencies in the methods under test and ensure the viability of the tests. You should be able move these unit tests into your project when you use the Locale user control. If you aren’t using the Visual Studio Test framework and are using NUnit instead, it is quite easy to change the syntax.



Download



You can download the full solution including the example MVC application, locale user control and unit tests below.




- Aaron

http://www.churchofficeonline.com

Essential Free Software For .NET Developers

Every software nerd has their suite of essential free applications they can’t live without, here’s mine:

Development Freebies

Visual Studio Express Editions http://www.microsoft.com/exPress/
Free versions of the most excellent IDE, Visual Studio 2008.

jQuery http://jquery.com/
Most awesomest javascript library ever; if I could marry it I would.

GhostDoc http://submain.com/products/ghostdoc.aspx
Makes XML method documentation in .NET very smooth.

NUnit http://www.nunit.org/index.php
Fast and lightweight unit testing library for .NET.

Firebug http://getfirebug.com/
_MOST USEFUL WEB DEVELOPMENT TOOL EVER CREATED_ Nuff said.

Moq http://code.google.com/p/moq/
Excellent mocking framework for dependency injection in unit tests.

NHibernate https://www.hibernate.org/343.html
Absolutely incredible ORM package, this is what entity framework should be. If you are using DataSets, entity framework, LINQ to Sql, etc. stop right now and go with Domain Driven Design and NHibernate.

Expresso http://www.ultrapico.com/Expresso.htm
Handy regular expression build, validation and testing tool.

OS Utilities & Application Freebies

Virtual Clone Drive http://www.slysoft.com/en/virtual-clonedrive.html
Incredibly useful iso mounting utility that allows you to instantly mount any iso into a drive.

Auslogics Disk Defrag http://www.auslogics.com/disk-defrag
Very nice free disk defrag app to replace the base Windows version (works in Windows 7.)

Notepad++ http://notepad-plus.sourceforge.net/uk/site.htm
AWESOME little app for editing XML, HTML, etc.

Windows Live Writer http://download.live.com/writer
Best blog post authoring application I have found.

7Zip http://www.7-zip.org/
Nice compression / extraction utility.

WinDirStat http://windirstat.info/
Easy and visual way to figure out where all of your hard disk space has gone.

Picasa http://picasa.google.com/
Incredible useful photo and video management app.

Paint.NET http://www.getpaint.net/
Sweet Photoshop Lite replacement (FREE!).

Digsby http://www.digsby.com
Excellent IM client that supports every protocol under the sun.

FoxIt Reader http://www.foxitsoftware.com/pdf/reader/
Lightweight PDF reader.

Virtual Box http://www.virtualbox.org/
Most excellent virtualization application. Puts Virtual PC to shame.

Google Analytics http://www.google.com/analytics/
Super awesome analytics for all of your web applications.

ASP.NET Session Expiration Redirect

Most of the applications I work with require some form of authentication and a timed expiration of the authentication ticket and session object. When the expiration takes place it can wreak havoc on your application code if you are relying on the Session object or have authentication code in place to ensure that the user is authenticated before you serve up any data for them.

Unfortunately ASP.NET doesn't provide a nice out of the box solution for handling the session timeout gracefully. Everything expires behind the scenes and your user is left unaware of what has happened. Additional problems can arise if the user abandoned their browser in a state that you didn't code for, which can result in errors or exceptions taking place.

With this in mind I like to put a Session Expired page in place in my applications and pro-actively send the user there when their session ends. This accomplishes a few different things:

1) Provides a nice user experience for your users
2) Prevents your application from being left in an unknown state when the session / auth ticket expires
3) Prevents application errors and exceptions from occurring when a user tries to perform an action on a page after their session / auth ticket has expired

Nested MasterPage(s) For All Authenticated Pages

You need to implement a MasterPage (or a Page base class) for all of your authenticated pages so you can inject a bit of javascript to handle the redirect on session timeout.

Get Your Web.config Settings Straight

Make sure that your auth ticket timeout and session timeout match.

<system.web> 
    <!-- 
        Session configuration 
        - Timeout value needs to match Forms Auth timeout** 
    --> 
    <sessionState timeout="45" /> 
    <!-- 
        The <authentication> section enables configuration 
        of the security authentication mode used by 
        ASP.NET to identify an incoming user. 
    --> 
    <authentication mode="Forms"> 
        <forms loginUrl="Login.aspx" 
           protection="All" 
           path="/" 
           requireSSL="false" 
           timeout="45" 
           name=".ASPXAUTH" 
           slidingExpiration="true" 
           defaultUrl="Login.aspx" 
           cookieless="UseDeviceProfile" 
           enableCrossAppRedirects="false" /> 
</system.web> 


Create Your Session Expiration Page



Create a page for your users to be taken to when their session expires. I created SessionExpired.aspx with the following message:



<h2>Session Expired</h2>                   
<p> 
    <span style="color:White;">Your session has expired due to inactivity.</span> 
    <br /> 
    <br /> 
    <a href="Login.aspx">Click here to login again</a> 
</p> 


Ensure the Authentication ticket is signed out in your code behind:



FormsAuthentication.SignOut();


Add The Session Expiration Javascript To Your Page_Load



In the Page_Load method of your MasterPage (or base page class) add the following code:



// Handle the session timeout 
string sessionExpiredUrl = Request.Url.GetLeftPart(UriPartial.Authority) + "/SessionExpired.aspx"; 
StringBuilder script = new StringBuilder(); 
script.Append("function expireSession(){ \n"); 
script.Append(string.Format(" window.location = '{0}';\n", sessionExpiredUrl)); 
script.Append("} \n"); 
script.Append(string.Format("setTimeout('expireSession()', {0}); \n", this.Session.Timeout * 60000)); // Convert minutes to milliseconds 
this.Page.ClientScript.RegisterClientScriptBlock(this.GetType(), "expirescript", script.ToString(), true);


That's It



The session timeout script should be injected into each of your authenticated pages now. The client's browser will begin the countdown after each page has loaded or each PostBack has occurred. Once the countdown is reached the browser will redirect to your SessionExpired page.



Reference:

http://msmvps.com/blogs/shahed/archive/2007/09/05/redirect-to-login-page-on-session-expiration-asp-net.aspx



Aaron Schnieder

http://www.churchofficeonline.com

The Power (and complexity) Of .NET Unit Testing

I have recently begun a journey down the path of unit testing. I have been engineering software for many years now and up until this point my testing has been limited to integration testing my code using a front end. While this type of testing is effective in certain instances, it is not thorough and leaves a lot of unconvered bugs. I am part of a very strong engineering team at the moment and one of the software design techniques they swear by is unit testing. As I learn more and more about unit testing the more I am sold on this concept.

Where To Begin
I purchased "The Art of Unit Testing" by Roy Osherove to get my head wrapped around this new software design paradigm; this book is awesome. A great book that covers the basics, the concepts, the theory as well as the application in real world scenarios. I highly recommend you get this book: http://www.manning.com/osherove/

Unit Testing Aids
Unit testing takes time to engineer, no doubt about it. However there are a number of great software packages that will make your unit testing life a lot easier. Whatever unit testing framework you end up choosing needs a good mocking framework to co-incide with it. With that in mind let me suggest TypeMock.

TypeMock Isolator - Aiding your ASP.NET unit testing efforts

Unit Testing ASP.NET? ASP.NET unit testing has never been this easy.

Typemock is launching a new product for ASP.NET developers – the ASP.NET Bundle - and for the launch will be giving out FREE licenses to bloggers and their readers.

The ASP.NET Bundle is the ultimate ASP.NET unit testing solution, and offers both Typemock Isolator, a unit test tool and Ivonna, the Isolator add-on for ASP.NET unit testing, for a bargain price.

Typemock Isolator is a leading .NET unit testing tool (C# and VB.NET) for many ‘hard to test’ technologies such as SharePoint, ASP.NET, MVC, WCF, WPF, Silverlight and more. Note that for unit testing Silverlight there is an open source Isolator add-on called SilverUnit.

The first 60 bloggers who will blog this text in their blog and tell us about it, will get a Free Isolator ASP.NET Bundle license (Typemock Isolator + Ivonna). If you post this in an ASP.NET dedicated blog, you'll get a license automatically (even if more than 60 submit) during the first week of this announcement.

Also 8 bloggers will get an additional 2 licenses (each) to give away to their readers / friends.

Go ahead, click the following link for more information on how to get your free license.

Aaron
http://www.churchofficeonline.com

How To Register An ASP.NET Custom Control

If you write a custom ASP.NET control you need to register the control either on the Page level or globally in your web.config so you can use the control on forms in your web application.

ASP.NET Page Level Custom Control Declaration

If you want to register your control per page that you use it on, you can use the following declaration at the top of your web form:
<%@ Register TagPrefix="myWebControls" Namespace="My.Website.Controls" Assembly="My.Website" %>

ASP.NET Site Level Custom Control Declaration

If you want to register your asp.net control(s) for you entire website you can use the following declaration in your web.config file:
<add tagPrefix="myWebControls" namespace="My.Website.Controls" assembly="My.Website"/>

Parser Error Message: Unknown server tag

If you pre-compile our web application and do not specify the assembly in your control declaration you will get the following error when trying to use your ASP.NET custom control:
Parser Error Message: Unknown server tag 'myWebControls:ViewEditTextControl'.

Aaron Schnieder
http://www.churchofficeonline.com