Monday, January 25, 2010

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

Thursday, January 21, 2010

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.

First off, if you are looking for an excellent jQuery book I HIGHLY recommend jQuery in Action

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

Sunday, January 3, 2010

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.