.NET Performance Blog

March 18, 2013

Blog name changes to “.NET Performance Blog”

Filed under: General — Eric P @ 5:51 am

April 5, 2011

OpenFaq (Part 3): Custom Membership Provider in Entity Framework Code First

Filed under: ASP.NET MVC,BDD,DDD,General,TDD — Eric P @ 4:33 am

Previous posts in OpenFaq series
OpenFaq (Part 1): The beginning
OpenFaq (Part 2): Business Requirements in Code
Source code in Codeplex
http://openfaq.codeplex.com/SourceControl/list/changesets

Why Custom Membership Provider

When creating new MVC web site – it is automatically configured to use SqlMembershipProvider. Before it can actually be used, the default tables need to be created in DB using spnet_regsql.exe tool here.
http://www.asp.net/security/tutorials/creating-the-membership-schema-in-sql-server-vb.
That creates a whole bunch of tables, views and stored procs. See link above.

I would like to be pretty consistent in my approach to how the database is accessed (through Entity Framework) and how my tables are named and structured. So I am going to use a flexible Provider model supplied my mine and your friends at Microsoft to implement a custom membership provider that will be driven by Code First Entity Framework.

YAGNI says only to implement things I need and worry about “things I don’t need” when I need them. So at this point in my application I just want to be able to register users, log them in and possibly change their password.

The acceptance tests written in part 2 of this series should still work.

Getting Samples from the Holy Book of MS (MSDN)

I used several examples provided by Microsoft for guidance:
http://msdn.microsoft.com/en-us/library/6tc47t75.aspx – simple example of ODBC Membership provider (C# after VB.NET code)
http://msdn.microsoft.com/en-us/library/aa478948.aspx – toolkit that includes fully featured example of Sql Membership/Role/Profile providers

A couple of interesting discoveries during implementation.

1. Some business logic code that should be re-usable is not provided in base MembershipProvider class

Two major cases of this were:
– Password Validation – like checking that password must be greater then 6 chars
– Password hashing – using Clear/Encrypted/Hashed

Both samples from Microsoft implement their own versions of validation and hashing.
In my case I had to implement my own (mostly by copying the toolkit sample).

I was hoping that provider model would allow to override these scenarios, but provide basic implementation which would be applicable in 90% of the cases.
Unfortunately that’s not the case.

2. Bad coding practices in sample

In Tooklit example there were a couple of things, that would make some developers punch the wall with their head.

The “Code Duplication” is the only major one, but since many developers are looking to microsoft for guidelines on how to write good code, I think MS should really make sure that their code has been properly reviewed and updated whenever there are new .NET features and guidelines. For ex. ODBC Membership provider sample is still using Hungarian notation which MS has been saying not to use for a while now.

Here are some examples of bad coding practices from Toolkit sample:

Code Duplication

Password validation logic is duplicated in CreateUser and ChangePassword

-- In CreateUser

           if( password.Length < MinRequiredPasswordLength )
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            int count = 0;

            for( int i = 0; i < password.Length; i++ )
            {
                if( !char.IsLetterOrDigit( password, i ) )
                {
                    count++;
                }
            }

            if( count < MinRequiredNonAlphanumericCharacters )
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }

            if( PasswordStrengthRegularExpression.Length > 0 )
            {
                if( !Regex.IsMatch( password, PasswordStrengthRegularExpression ) )
                {
                    status = MembershipCreateStatus.InvalidPassword;
                    return null;
                }
            }

            string salt = GenerateSalt();
            string pass = EncodePassword(password, (int)_PasswordFormat, salt);
            if ( pass.Length > 128 )
            {
                status = MembershipCreateStatus.InvalidPassword;
                return null;
            }


-- In ChangePassword
  

            if( newPassword.Length < MinRequiredPasswordLength )
            {
                throw new ArgumentException(SR.GetString(
                              SR.Password_too_short,
                              "newPassword",
                              MinRequiredPasswordLength.ToString(CultureInfo.InvariantCulture)));
            }

            int count = 0;

            for( int i = 0; i < newPassword.Length; i++ )
            {
                if( !char.IsLetterOrDigit( newPassword, i ) )
                {
                    count++;
                }
            }

            if( count < MinRequiredNonAlphanumericCharacters )
            {
                throw new ArgumentException(SR.GetString(
                              SR.Password_need_more_non_alpha_numeric_chars,
                              "newPassword",
                              MinRequiredNonAlphanumericCharacters.ToString(CultureInfo.InvariantCulture)));
            }

            if( PasswordStrengthRegularExpression.Length > 0 )
            {
                if( !Regex.IsMatch( newPassword, PasswordStrengthRegularExpression ) )
                {
                    throw new ArgumentException(SR.GetString(SR.Password_does_not_match_regular_expression,
                                                             "newPassword"));
                }
            }

            string pass = EncodePassword(newPassword, (int)passwordFormat, salt);
            if ( pass.Length > 128 )
            {
                throw new ArgumentException(SR.GetString(SR.Membership_password_too_long), "newPassword");
            }
            ...

The logic from both functions should be combined into one method like ValidatePassword, which returns enough information to generate proper exception or return error code in calling function.

Using try/catch with no logic in catch

Throughout the toolkit sample there is code like

try {
               //some code
} catch {
                throw;
            }

If you ever stumble over code like above, I would recommend rewriting it like this and then sending it to original coder for review:

DoNothing();
//some code
DoNothing();
DoNothing();
DoNothing();


private void DoNothing()
{
     //Do nothing
}  

On the bright side, it is not as bad as

try {
               //some code
} catch {
              //do nothing, to make sure that no exceptions are ever visible to user or the poor poor developer who will be maintaining this code
}

but nothing really is…

functions that have 10 input and 10 out parameters

   private void GetPasswordWithFormat( string       username,
                                            bool         updateLastLoginActivityDate,
                                            out int      status,
                                            out string   password,
                                            out int      passwordFormat,
                                            out string   passwordSalt,
                                            out int      failedPasswordAttemptCount,
                                            out int      failedPasswordAnswerAttemptCount,
                                            out bool     isApproved,
                                            out DateTime lastLoginDate,
                                            out DateTime lastActivityDate)

Uncle Bob just started spontaneously crying…

Not sure when this sample was written, but unless it was written on punch cards, they must have heard about passing in and returning structures, instead of 10+ args.
Also things like single responsibility principle – why does function called GetPasswordWithFormat return lastLoginDate.

Implementing the beast

From business requirements perspective, at this point I would like to support:
Register
Login
Change password

So I implemented the following functions in new CustomMembershipProvider class:
CreateUser
ValidateUser
ChangePassword

Naming Unit Tests

For each function that I implemented in CustomMembershipProvider I created unit tests first.
I used format
{Function}_With{Description}_Should{Result}
for ex…
CreateUser_With_Valid_Data_Returns_User()

Then I found a nicer format:
[MethodName_StateUnderTest_ExpectedBehavior] from:
http://stackoverflow.com/questions/155436/unit-test-naming-best-practices

So the previous example is now:
CreateUser_WithValidData_ReturnsUser()

I also wanted to include “given” conditions (used in BDD), so it would be:
CreateUser_GivenDefaultMembershipProvideSettings_WithValidData_ReturnsUser()

At the end I changed “With” to “When”, so that we would use the same language for both BDD and TDD and I ended up with:
{Function}_Given{Precondition1}_Given{Precondition2}_When{Action}_{Result}
so example would be:
CreateUser_GivenDefaultMembershipProviderSettings_WhenValidData_ReturnsUser

Here are all the unit tests that should demonstrate what is currently implemented, for your viewing pleasure:

		[TestMethod]
		public void CreateUser_GivenDefaultMembershipProviderSettings_WhenValidData_ReturnsUser();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresMin6Chars_WhenPasswordHasLessThen6Chars_ReturnsNullAndStatusInvalidPassword();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresMin6Chars_WhenPasswordHasMoreThen6Chars_ReturnsUser();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresOneNonAphaNumericCharacter_WhenPasswordHasNoNonAlphaNumericCharacters_ReturnsNullAndStatusInvalidPassword();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresOneAphaNumericCharacter_WhenPasswordHasOneAlphaNumericCharacter_ReturnsUser();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresMatchRegularExpressions_WhenPassswordNotMatchingRegularExpreation_ReturnsNullAndStatusInvalidPassword();

		[TestMethod]
		public void CreateUser_GivenPasswordRequiresMatchRegularExpressions_WhenPassswordMatchingRegularExpreation_ReturnsNullAndStatusInvalidPassword();

		[TestMethod]
		public void CreateUser_WhenUsernameNotUnique_ReturnsNullAndStatusDuplicateUserName();

		[TestMethod]
		public void CreateUser_GivenPasswordFormatHashed_HashesPassword();

		[TestMethod]
		public void ChangePassword_WhenValidArguements_ChangesPassword();

		[TestMethod]
		public void ValidateUser_WhenUserWithUsernameAndPasswordExists_ReturnsTrue();

		[TestMethod]
		public void ValidateUser_WhenUserWithUsernameAndPasswordDoesNotExist_ReturnsFalse();

Deep Thoughts

In this episode of OpenFaq (The Series), we looked at how to create a simple Custom Membership Provider that uses EF Code First for back-end.
Coding crimes committed by MS where brought to light with appropriate punishment being administered as we speak.
TDD and BDD is still being followed.

For now you can get all the code discussed in this post from here:
http://openfaq.codeplex.com/releases/view/63833

Coming up next…

Now that we have the basic framework down, we are gonna implement us some FAQ goodness…

Current project road map is here:
http://openfaq.codeplex.com/wikipage?title=Road%20Map%20%26%20Progress

March 28, 2011

OpenFaq (Part 2): Business Requirements in Code

Filed under: .NET,ASP.NET MVC,BDD,General,TDD — Eric P @ 3:38 am

Previous posts in OpenFaq series
OpenFaq (Part1): The beginning

There has been a lot of discussion about self documenting code. You can use good class/method names, proper unit tests and comments to make it clear what the code does.
But how do you specify and enforce what the code is SUPPOSED to do from business perspective, versus what it does? Usually there is a separate Business requirements document with a set of User Acceptance criteria that is used to QA the application. What if you could automate user acceptance criteria tests and run them from beginning (even before writing any code). That’s where BDD comes in.

The Joy of BDD

Brandon Santrom has a nice presentation on using SpecFlow, WatiN with MVC to write acceptance tests:

Video is here:
http://channel9.msdn.com/Series/mvcConf/mvcConf-2-Brandom-Satrom-BDD-in-ASPNET-MVC-using-SpecFlow-WatiN-and-WatiN-Test-Helpers

MSDN article (that covers a bit different scenario) is here:
http://msdn.microsoft.com/en-us/magazine/gg490346.aspx

In the presentations, Brandon Santrom promotes the following way of doing devlelopment:
BDD Approach

The first module I wanted to implement for OpenFaq was UserModule which will handle CRUD for User and related objects and will also implement Custom Membership Provider that will use EF4.
But before doing that, following BDD, I will write some UAC tests to make sure that user can login and register using default Membership Provider that can be used with MVC 3.

Setting up solution

Before writing any tests I am going to setup a new solution/project for OpenFaq.
Following YAGNI, I will only create projects as I need them.

To Start I am going to have 3 projects:

  • OpenFaq.Web – MVC 3 project
  • OpenFaq.Web.Tests – unit tests for controllers
  • OpenFaq.AcceptanceTests – acceptance tests

I also setup NUGet package manager that ScottGu mentioned many times in his blog.
I used it to add references to WatiN, SpecFlow for “OpenFaq.AcceptanceTests” project. I noticed that NUGet was installing packages into “packages” directory under solution directory. Since my project structure is:

\src – solution goes here
\lib – external dependencies go here

So I changed NuGet package directory to put packages into “\lib” using instructions here:
http://stackoverflow.com/questions/4092759/is-it-possible-to-change-the-location-of-packages-for-nuget

Login & Register – Starting BDD

Before writing any new code I create the following two Features using SpecFlow:

Login Feature

Feature: Login a site user
	In order to use OpenFaq features
	As a site user
	I want to be able to login to the OpenFaq site

Scenario: Login with valid information
	Given I am on the site home page
	When I click the "Log On" link
	And I complete the form with the following information:
		| Field           | Value							|
		| UserName        | openfaquser1					|
		| Password        | password1						|
	And I click the "Log On" button
	Then I should see a link with the text "Log Off" on the page


Scenario: Login with invalid information
	Given I am on the site home page
	When I click the "Log On" link
	And I complete the form with the following information:
		| Field           | Value							|
		| UserName        | unknowuser						|
		| Password        | password1						|
	And I click the "Log On" button
	Then I should see a validation summary "Login was unsuccessful"
	And  I should see a field error "The user name or password provided is incorrect"

Register Feature

Feature: Register a new site user
	In order to ask/answer questions
	As a site user
	I want to be be able to register new account

@mytag
Scenario: Register with valid information
	Given I am on the site home page
		And I click the "Log On" link
		And I click the "Register" link	
	When I enter a random username
		And I complete the form with the following information:
			| Field				| Value					|
			| Email				| openfaquser@test.com	|
			| Password			| p@bla12				|
			| ConfirmPassword	| p@bla12				|
		And I click the "Register" button
	Then I should see a link with the text "Log Off" on the page

SpecFlow uses language called Gerhkin to describe business requirements. Not quite English and not quite code it is meant to bridge a gap between software developers and business analysts.
When you create SpecFlow feature files above, SpecFlow automatically creates CS files that interpret Gerhkin into C# code. When you run the tests, each step Given, When, Then… expects there to be a function that actually implements this functionality.

So to implement “Scenario: Login with valid information”, here is the file you would provide with Step Definitions:

using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenFaq.AcceptanceTests.StepHelpers;
using TechTalk.SpecFlow;
using WatiN.Core;

namespace OpenFaq.AcceptanceTests.Steps
{
	[Binding]
	public class Login
	{

		[Given(@"I am on the site home page")]
		public void GivenIAmOnTheSiteHomePage()
		{
			WebBrowser.Current.GoTo("http://localhost/OpenFaq.Web");
		}

		[When("I click the \"(.*)\" link")]
		public void WhenIClickALinkNamed(string linkName)
		{
			var link = WebBrowser.Current.Link(Find.ByText(linkName));

			if (!link.Exists)
				Assert.Fail(string.Format("Could not find '{0}' link on the page", linkName));

			link.Click();
		}

		[When(@"I complete the form with the following information:")]
		public void WhenICompleteTheFormWithTheFollowingInformation(Table table)
		{
			foreach (var tableRow in table.Rows)
			{
				var field = WebBrowser.Current.TextField(Find.ByName(tableRow["Field"]));

				if (!field.Exists)
					Assert.Fail(string.Format("Could not find {0} field on the page", field));

				field.TypeText(tableRow["Value"]);
			}
		}

		[When("I click the \"(.*)\" button")]
		public void WhenIClickAButtonWithValue(string buttonValue)
		{
			var button = WebBrowser.Current.Button(Find.ByValue(buttonValue));

			if (!button.Exists)
				Assert.Fail(string.Format("Could not find '{0}' button on the page", buttonValue));

			button.Click();
		}

		[Then("I should see a link with the text \"(.*)\" on the page")]
		public void ThenIShouldSeeALinkWithTheTextOnThePage(string linkText)
		{
			Assert.IsTrue(WebBrowser.Current.Link(Find.ByText(linkText)).Exists,
				string.Format("The following link text was not found on the page: {0}", linkText));
		}
	}
}

You may have noticed that in many cases instead of specifying actual text, I use a regular expression to pass Button value or link text to the function.

[Then("I should see a link with the text \"(.*)\" on the page")]

//instead of

[Then("I should see a link with the text \"Log On\" on the page")]

This will allow me to re-use the steps, in many different scenarios.

MembershipProvider – where art thou

When I ran acceptance tests I received errors having to do with MembershipProvider not being setup. To set it up on my local Sql Server DB I used steps here:
http://helios.ca/2009/04/22/aspnet-mvc-sqlmembershipprovider/

Now all acceptance tests have passed.

Tests Passed

Yes, I do use Resharper 5.1.

Did they live happily ever after?

Not quite yet…

In next part of this series I will replace SqlMembershipProvider with the one that supports EF 4.0 Code First.

For now you can get all the code from here:
http://openfaq.codeplex.com/releases/view/63346

Current project road map is here:
http://openfaq.codeplex.com/wikipage?title=Road%20Map%20%26%20Progress

March 27, 2011

OpenFaq (Part 1): The beginning

Filed under: .NET,ASP.NET,ASP.NET MVC,DDD,General,TDD — Eric P @ 1:43 pm

It is time to learn some new technologies.

For this task I am going to write FAQ applicaton called OpenFaq.

OpenFaq – The future of FAQ

Whenever we buy any new product there is usually some questions or issues that come up, that may not be covered in manual or by asking the uncle who (supposedly) knows everything. How do I …? Why am I getting this f#$x error? etc…

In some cases you may go to product’s website and try to find an answer there. In about 5 minutes (depending on how quickly you reach the boiling point) you give up and just do a google search.

Why are so many sites so bad in helping you find what you need?

The problem(s) this application will try to solve is:
On many sites FAQ is created early on and not frequently updated. It quickly becomes forgotten.
The questions are grouped according to how website administrator feels like they should be grouped. Not based on frequency of asking.
User interaction is limited to reading an answer

The Solution
Make FAQ dynamic, so admin can edit questions and answers without doing rollout
Get users involved with asking, answering, voting and comments (similar to stack overflow)
Use voting and other statistics to determine which questions are frequent/popular and which are not

Some Business Requirements

Some preliminary requirements for this application are:
* User can login
* User can post question
* User can answer question
* User can comment on question and answer
* User can vote on question and answer
* User can use keyword search to quickly find questions, answers

Maybe a touch of Technology

In this series I will try out some new technologies from MS and use some of the latest methodologies in application development.

For a while now I have been reading about many new technologies and approaches for software development. Here is the list that I plan to use for OpenFaq.

Approaches

  • BDD – behavior driven development
  • TDD – test driven development

Principles

  • YAGNI – you are not going to need it
  • KISS – keep it simple stupid
  • DRY – don’t repeat yourself

Technology

For this project I am going to use Microsoft stack.

  • ORM – EF 4 Code First
  • Web Framework – MVC 3
  • Template Engine – Razor
  • Package/Dependecy Manager – NuGet
  • BDD – SpecFlow
  • UI Testing/Acceptance test – WatiN
  • Unit testing – MS Test
  • Version Control – Mercurial
  • Project Site – Codeplex.com

February 2, 2010

ASP.NET GridView – edit records using JQuery Dialog

Filed under: .NET,ASP.NET,JQuery — Eric P @ 6:25 am

There are many ways in which you could modify modular data in Grid View:
1. Inline by making rows editable when u click on Edit
2. By using separate pages for Edit
3. By using popups/dialogs for adding/editing

In this post I will show how to implement the 3rd approach using JQuery UI Dialog for editing/adding data in ASP.NET GridView.

The following features have been implemented:
1. List customers
2. Add customer – Save/Cancel
3. Edit customer – Save/Cancel
4. Delete customer
5. Server side validation
6. Edit dialogs appear next to trigger links

You can check out the demo here:
http://samples.entechsolutions.com/GridWithEditDialog

The code is available here:
http://samples.entechsolutions.com/Downloads/GridWithEditDialog.zip

Implementation

Data source

The grid lists customers which are stored in list which is initialized for each new session. This way each user trying out this sample will not affect other users.
CustomerService is a class wrapped around list of customers that allows to add/edit/list customers:

public class CustomerService
{
	private List<Customer> Customers
	{
		get
		{
			List<Customer> customers;
			if (HttpContext.Current.Session["Customers"] != null)
			{
				customers = (List<Customer>)HttpContext.Current.Session["Customers"];
			}
			else
			{
				//Create customer data store and save in session
				customers = new List<Customer>();

				InitCustomerData(customers);

				HttpContext.Current.Session["Customers"] = customers;
			}

			return customers;
		}
	}


	public Customer GetByID(int customerID)
	{
		return this.Customers.AsQueryable().First(customer => customer.ID == customerID);
	}


        ...
        //Add/Update/Delete/GetAll

Add/Edit dialog

There are two main operations that are handled using JQuery dialog
* Add Customer
* Edit Customer

In both cases the same dialog is used, as well as the same content div and Update Panel

	<div id="divEditCustomerDlgContainer">	
			<div id="divEditCustomer" style="display:none">
					
				<asp:UpdatePanel ID="upnlEditCustomer" runat="server">
					<ContentTemplate>
						<asp:PlaceHolder ID="phrEditCustomer" runat="server">
							<table cellpadding="3" cellspacing="1">
							<tr>
								<td>
									*First Name:
								</td>
								<td>
									<asp:TextBox ID="txtFirstName" Columns="40" MaxLength="50" runat="server" />
								        ...
								</td>
							</tr>
							<tr>
								<td>
									*Last Name:
								</td>
								...
							</tr>
							<tr>
								<td colspan="2" align="right">
									<asp:Button ID="btnSave" onclick="btnSave_Click" Text="Save" runat="server" />
									<asp:Button ID="btnCancel" onclick="btnCancel_Click" onClientClick="closeDialog()" CausesValidation="false" Text="Cancel" runat="server" />
								</td>
							</tr>
							</table>
								
						</asp:PlaceHolder>
					</ContentTemplate>
							
				</asp:UpdatePanel>
			
			</div>
		</div>	<!-- divEditCustomerDlgContainer -->

You may notice that there are two divs around UpdatePanel – divEditCustomerDlgContainer and divEditCustomer. The reason for that is that when JQuery dialog opens – it is added after FORM tag in dom. That disables any ASP.NET submits and server side validation. The issue is described here:
http://www.trentjones.net/index.php/2009/03/jqueryui-dialog-with-aspnet-empty-post-values

To fix the issue I added a handler for JQuery Dialog open event that takes JQuery dialog DOM object and inserts it as a child of “divEditCustomerDlgContaine”. Note that JQuery dialog references divEditCustomer.

	<script type="text/javascript">
		$(document).ready(function() {
			$("#divEditCustomer").dialog({
				autoOpen: false,
				modal: true,
				minHeight: 20,
				height: 'auto',
				width: 'auto',
				resizable: false,
				open: function(event, ui) {
					$(this).parent().appendTo("#divEditCustomerDlgContainer");    //This is where JQuery dialog is added to DlgContainer
				},
			});
		});
     ...

When user clicks on Add/Edit customer links the following steps are followed:
1. JQuery dialog is opened
2. Content of dialog is overlayed with AJAX indicator and white background – BlockUI is used for that purpose
3. AJAX call is made that triggers refresh of upnlEditContent
4. On server side – dialog content is cleared for adding or loaded for editing.
5. Server side uses RegisterStartupScript to trigger a javascript call to UnlockDialog
6. Dialog is unlocked and available to user for editing

With this approach user sees the dialog right away and indicator tells him that data is being loaded. In most cases it should be pretty fast from click to being able to edit.

Positioning Dalog

You may notice that dialog appears to the right of the link that you click on. For that JQuery position() functionality is used:

	function openDialog(title, linkID) {
	
		var pos = $("#" + linkID).position();
		var top = pos.top;
		var left = pos.left + $("#" + linkID).width() + 10;
		
		
		$("#divEditCustomer").dialog("option", "title", title);
		$("#divEditCustomer").dialog("option", "position", [left, top]);
		
		$("#divEditCustomer").dialog('open');
	}

linkID is the ClientID of the link. Using the position() u can display dialog anywhere on the page or in relation to another control.

Running Client Side code using RegisterStartupScript in Async

To run some javascript script when response comes back from asynchronous request – function ScriptManager.RegisterStartupScript can be used.

The issue is that the first parameter in:
RegisterStartupScript Method (Control, Type, String, String, Boolean)

must be a control in update panel that will be rendered on the current request. In the “List Customers” page there are two update panels -“upnlEditCustomers” and “upnlCustomers” (for grid view).
The first panel is always updated on any async request (UpdateMode=”Always” by default). Originally I though to make it conditional which would be more efficient, but there were some issues to resolve and I decided it wasn’t worth the trouble in this case. As for “upnlCustomers” – we definitely don’t want that re-rendered on every async request – like opening/canceling the dialog, so this one is set to update conditionally.

To get RegisterStartupScript to work without worrying that triggering conrol is rendered in current request, I added another UpdatePanel whose sole purpose is to facilitate JavaScript calls from Async requests. This panel has an empty placeholder that’s used as a control in RegisterStartupScript:


	<asp:UpdatePanel ID="upnlJsRunner" UpdateMode="Always" runat="server">
		<ContentTemplate>
			<asp:PlaceHolder ID="phrJsRunner" runat="server"></asp:PlaceHolder>
		</ContentTemplate>
	</asp:UpdatePanel>

On server side there is a function RegisterStartupScript(key, script) that uses update panel above:

	private void RegisterStartupScript(string key, string script)
	{
		ScriptManager.RegisterStartupScript(phrJsRunner, phrJsRunner.GetType(), key, script, true);
	}

        private void TriggerClientGridRefresh()
	{
		string script = "__doPostBack(\"" + btnRefreshGrid.ClientID + "\", \"\");";
		RegisterStartupScript("jsGridRefresh", script);   //Trigger async grid refresh on Save
	}

	private void HideEditCustomer()
	{
		ClearEditCustomerForm();
		RegisterStartupScript("jsCloseDialg", "closeDialog();");    //Close dialog on client side
	}

Clear data in dialog

When user clicks on “Add Customer” – data in dialog is cleared and all validators are reset to Valid. For this I wrote a function that loops through all the controls in dialog and then performs a certain action on the controls of certain type:

	private void ClearEditCustomerForm()
	{
		//Empty out text boxes
		var textBoxes=new List<Control>();
		FindControlsOfType(this.phrEditCustomer, typeof(TextBox), textBoxes);
		
		foreach (TextBox textBox in textBoxes)
			textBox.Text = "";

		//Clear validators
		var validators=new List<Control>();
		FindControlsOfType(this.phrEditCustomer, typeof(BaseValidator), validators);
	
		foreach (BaseValidator validator in validators)
			validator.IsValid = true;
	}


	static public void FindControlsOfType(Control root, Type controlType, List<Control> list)
	{
		if (root.GetType() == controlType || root.GetType().IsSubclassOf(controlType))
		{
			list.Add(root);
		}

		//Skip input controls
		if (!root.HasControls())
			return;

		foreach (Control control in root.Controls)
		{
			FindControlsOfType(control, controlType, list);
		}
	}

Right now it only handles TextBoxes and Validators, but it can be easily expanded to support Text Areas, Drop Downs, etc…

JQuery Includes

This sample doesn’t include the files for JQuery UI and JQuery base library. Instead those are referenced at the google AJAX CDN.
The approach for including JQuery UI from Google AJAX CDN is covered here: http://encosia.com/2009/10/11/do-you-know-about-this-undocumented-google-cdn-feature/

<link href="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/themes/start/jquery-ui.css" type="text/css" rel="Stylesheet" />
	
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.0/jquery.min.js"></script>
<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jqueryui/1.7.2/jquery-ui.min.js"></script>

This makes the application and code download a lot smaller (a lot less files), so it is great for sample projects.

December 4, 2009

Integrating FreeText Search in NHibernate Detached Criteria

Filed under: General — Eric P @ 5:01 am

Sql Server 2005/2008 has very nice FreeText searching capabilities. It is pretty easy to run FreeText query in Sql, but how would one do it in NHibernate.

To be able to use “contains” or “freetext” functions in HQL, the functions need to be registered in an override of MsSql2008Dialect, as is done here:
http://nhforge.org/blogs/nhibernate/archive/2009/03/13/registering-freetext-or-contains-functions-into-a-nhibernate-dialect.aspx

But how would one do it in Criteria or DetachedCriteria. The approach I chose was to implement a new class based on ICriterion, AbstractCriterion. To simplify the process I downloaded latest NHibernate 2.1 and based my class on one of the already existing ICriterion implementations.

You will need NHibernate 2.1 and FreeText functionality enabled on your Sql Server database. Also all the text columns that are searched using FreeText Search, must be indexed in a FreeText catalog.
Here is the code for ContainsExpression class which allows for using Criteria API for simple “CONTAINS” searches:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NHibernate;
using NHibernate.Criterion;
using NHibernate.Engine;
using NHibernate.SqlCommand;
using NHibernate.Util;

namespace  ENTech.Common.Infrastructure
{
	public class ContainsExpression : AbstractCriterion
	{
		private readonly string propertyName;
		private readonly object value;
		private readonly IProjection projection;


		public ContainsExpression(IProjection projection, object value)
		{
			this.projection = projection;
			this.value = value;
		}


		public ContainsExpression(string propertyName, object value)
		{
			this.propertyName = propertyName;
			this.value = value;
		}


		public override SqlString ToSqlString(ICriteria criteria, ICriteriaQuery criteriaQuery,
		                                      IDictionary<string, IFilter> enabledFilters)
		{
			//TODO: add default capacity
			SqlStringBuilder sqlBuilder = new SqlStringBuilder();
			SqlString[] columnNames =
				CriterionUtil.GetColumnNames(propertyName, projection, criteriaQuery, criteria, enabledFilters);

			SqlString columnName = columnNames[0];

			criteriaQuery.AddUsedTypedValues(GetTypedValues(criteria,criteriaQuery));
			sqlBuilder
				.Add("contains(")
				.Add(columnName)
				.Add(StringHelper.CommaSpace);

			sqlBuilder.AddParameter();
			sqlBuilder.Add(")");
			

			return sqlBuilder.ToSqlString();
		}


		public override TypedValue[] GetTypedValues(ICriteria criteria, ICriteriaQuery criteriaQuery)
		{
			return CriterionUtil.GetTypedValues(criteriaQuery, criteria, projection, propertyName, value.ToString().ToLower());
		}

		public override IProjection[] GetProjections()
		{
			if(projection != null)
			{
				return new IProjection[] { projection };
			}
			return null;
		}


		/// <summary></summary>
		public override string ToString()
		{
			return (projection ?? (object)propertyName) + " contains " + value;
		}
	}
}

The main method in this class is “ToSqlString” which generates Sql from Expression parameters.

Here is the unit test that checks this functionality.
It basically gets a count of all non-deleted Products with name or description that contain “pants” word.

using System;
using Castle.ActiveRecord;
using ENTech.Common.Infrastructure;
using NHibernate.Criterion;
using NUnit.Framework;
using Modules.ProductCatalog;

namespace ENTech.Common.Infrastructure.Tests
{
    [TestFixture]
    public class NHibernateTest
    {
        
		[Test]
		public void Search_For_Pants_Using_DatachedCriteria_With_ContainsExpressioin()
		{
			using (new SessionScope())
			{
				string keyword = "pants";

				var query = DetachedCriteria.For<Product>();

				query.Add(Restrictions.IsNull("DateDeleted"));

				ICriterion criterion = new ContainsExpression("Name", keyword);
				criterion = Restrictions.Or(criterion, new ContainsExpression("Description", keyword));

				query.Add(criterion);

				Console.WriteLine("Total products: " + ActiveRecordMediator.Count(typeof(Product), query));
			}
		}
	}
}

Using NHProf. I can see that the following sql query is generated:

SELECT count(* ) as y0_
FROM   Product this_
WHERE  this_.DateDeleted is null 
       and (contains(this_.Name,'pants' /* @p0 */)
             or contains(this_.Description,'pants' /* @p1 */))

July 25, 2009

ASP.NET MVC Grid – Part 7 – Reusable control

Filed under: ASP.NET MVC — Eric P @ 10:53 am

At this point all the features I planned in Part 1 have been implemented. It is time to make grid re-usable.

Click on image to view the whole screenshot

Click on image to view the whole screenshot

You can see demo here:
http://samples.entechsolutions.com/MvcGridSample/Part7

Code is available here:
http://samples.entechsolutions.com/Downloads/MvcGridSample/MvcGridSample_Part7.zip


So how does one make a grid into reusable control. There is no standard approach in MVC to separate both Logic and Presentation, similar to User Controls in web forms. I hope one will be provided in MVC 2, but for now it is up to a developer to pick from several options.

A found a good article on consolidating HTML for re-usability here: http://www.ajaxprojects.com/ajax/tutorialdetails.php?itemid=487#start. It includes various approaches and strengths and weaknesses of each one.

I considered the following options that would allow me to separate both Grid logic and UI.

a. Extending HtmlHelper

An example of using HtmlHelper to generate a simple pager is here: http://blogs.taiga.nl/martijn/?s=pager
I think helpers are to be better left for building very small HTML pieces, like action links. Even a pager in the example above may be too complex for HtmlHelper.

b. RenderAction

This was mentioned by Rob Connery here:
http://blog.wekeroad.com/blog/asp-net-mvc-preview-4-componentcontroller-is-now-renderaction/

This approach mimics User Control in WebForms, but instead of code behind – you need provide a separate controller to handle Grid Actions. There are some reservations to using this approach described here:
http://haacked.com/archive/2008/07/16/aspnetmvc-codeplex-preview4.aspx

Some people comment that this is not a “Valid” MVC approach, since view would have knowledge of Controllers.

This approach works best for controls like Login form which exist on multiple pages or master page, but the functionality is always the same. In case of the Grid control – the functionality will be custom depending on the entity being desplayed, so RenderAction doesn’t exactly fit here (even though it could work).

c. Partial views
In this approach partial views are used to encapsulate common HTML. All the logic goes into the parent controller. This approach may be the best option for now.


Implementation

The first thing to do to make control re-usable is “Find What Is Varying and Encapsulate It”. The simplest way to do that is to create another grid for a different entity. Then look for code duplication and encapsulate it into shared views/classes.

Add Product Entity

I created a new Product entity that looks like this:


public class Product
{
	[Required(ErrorMessage = "Required.")]
	public int ID { get; set; }

	[Required(ErrorMessage = "Required.")]
	public string Name { get; set; }

	[Required(ErrorMessage = "Required.")]
	[StringLength(10, ErrorMessage = "Must be 10 characters.", MinimumLength = 10)]
	public string SKU { get; set; }

	public string Description { get; set; }

	[Required(ErrorMessage = "Required.")]
    public decimal Price { get; set; }
}

Then I created ProductService class, ProductController and all the Views to List and Edit products

\Models
     Product.cs
     ProductService.cs -  Add/Update/GetByID for product

\Controllers
     ProductController.cs

\Views
    \Product
        List.aspx
        Edit.aspx
        _Grid.aspx

Finding what varies

In Each Grid – the following pieces will work and look the same:

  • Keyword Search and switching search modes
  • Pager navigation and size
  • Sorter
  • CSS and JS

Mvc_Grid_SamePieces
And the following pieces will vary:

  • Column and row data in grid table
  • Advanced Search form

Mvc_Grid_VariedPieces

Here is the screen shot of product list for comparison
Mvc_Grid_Products_List

Extracting Partial views

To incapsulate common UI, while allowing users to only implement pieces that vary I separated Grid html into 4 partials:

_Grid.aspx – generic grid display
_SearchForm.aspx – generic search form
_GridData.aspx – custom display of Grid Data
_AdvancedSearchForm.aspx – custom advanced search form

The Views directory structure looks like:


\Views
    \Customer
        List.aspx  - includes RenderPartial for _Grid and _SearchForm in /Views/Shared
        Edit.aspx
        _GridData.aspx - data table for displaying Customer entries
        _AdvancedSearchForm.aspx - avanced search fields to find Customers
    
    \Product
        List.aspx
        Edit.aspx
        _GridData.aspx - data table for displaying Product entries
        _AdvancedSearchForm.aspx - avanced search fields to find Products

    \Shared
        _Grid.aspx 
             - generic grid display - that provide Grid structure with header and paging, 
                but leaves it up to custom  _GridData (for ex. in \Views\Customer) to display columns and rows
        _SearchForm.aspx 
             - generic search form with Keyword search and link to switch to Advanced.  For custom Advanced Search it 
               will use _AdvancedSearchForm located in appropriate Views directory like (Views\Customer) 
        _Pager.aspx -  to be able to repeat pager on top and bottom of the grid

Passing view models

To be able to pass information from \Customer\List.aspx to \Shared\_Grid.asmx and then back to \Customer\_GridData.aspx – I created an interface that would allow me to box and un-box grid data. I extracted IGrid interface from Grid and passed to shared views:

public interface IGrid
{
	Pager Pager { get; set; }
	Sorter Sorter { get; set; }
	GridAction GridAction { get; set; }
	bool IsEmpty { get; }
	SelectList PageSizeSelectList();
	string PageNavActionLink(string linkText, int page);
	string SortActionLink(string linkText, string sortField);
}

Here is the whole workflow:

a. Customer\List.aspx renders two partial views “_SearchForm.aspx” and “_Grid.aspx”

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<CustomerGrid>" %>
...
<div id="search-form">
	<% Html.RenderPartial("_SearchForm", Model.SearchForm); %>
</div> <!-- search-form -->
	
<div style="clear: both"></div>
		
<div id="grid">
	<% Html.RenderPartial("_Grid", Model); %>
</div>  <!-- grid -->	
...

b. _Grid.aspx takes IGrid as ViewModel and passes it on to \Shared\_Pager.ascx and \Customer\_GridData.ascx

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IGrid>" %>
...
<div class="pager-nav">			
	<% Html.RenderPartial("_PagerNav", Model); %>
</div>
...
<div id="grid-data">
	<% Html.RenderPartial("_GridData", Model); %>
</div> <!-- data -->
...

c. Partial Customer\_GridData.ascx automatically unboxes IGrid into CustomerGrid

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<CustomerGrid>" %>
...

d. Move the search logic outside of controllers

When going through ProductController and CustomerController code – I noticed that both contained a lot of LINQ search logic. For better separation of concerns, I moved the search logic into Model/Domain classes: CustomerCriteria and ProductCriteria. This leaves controllers to do what they do best: display logic.


Observations

Conclusion and what is next…

In this part, I was able to re-use the Grid for both Customers and Products by moving all the common functionality into shared partial views.

There is still a bit of code that needs to go into the Controller to fill Grid data and save/load grid state. I was thinking of moving this code into base controller class, but this could introduce some coupling. I would like to see if RenderAction may be a better solution.

In the near future I will be using Grid control in several client projects. That will be a great opportunity to smooth out any rough edges. I also plan to introduce some of the more advanced features like: Batch Actions with check boxes, multi-column sorting and inline editing. So stay tuned…

In the near future I will add the following bug fixes/enhancements:
1. Use namespace for Grid’s javascript to provide for a better encapsulation
2. Fix a bug where user can’t switch to advanced search if JS is disabled
3. Fix a bug where server side errors are not displayed for Advanced Search, when date or numeric fields are invalid
4. Introduce unit tests for grid actions (other then Paging which already has unit tests)
5. CSS Bug fix: There is a white dot that appears in FireFox right under header row.

July 17, 2009

ASP.NET MVC Grid – Part 6 – Advanced Search, AJAX, Users with Javascript Disabled

Filed under: ASP.NET MVC — Eric P @ 8:06 am

In this part I will implement:

  • advanced search
  • using AJAX for grid actions
  • support for users with Javascript disabled
  • separate grid.css and grid.js

Click image to view the full screenshot

Click image to view the full screenshot


You can see demo here:
http://samples.entechsolutions.com/MvcGridSample/Part6

Code is available here:
http://samples.entechsolutions.com/Downloads/MvcGridSample/MvcGridSample_Part6.zip

Implementation

1. Advanced Search
Advanced Search differs from Keyword Search in a way that users can search for customers using one or more fields, for example – find all customers with first name “John” and with last order placed in the last month.

To implement advanced search I extended SearchForm class to include fields like First Name, Last Name, Phone, etc…


public class SearchForm
{
	//Properties
	public bool IsAdvanced { get; set; }
	public string Keyword { get; set; }
}

public class CustomerSearchForm : SearchForm
{
	public string FirstName { get; set; }
	public string LastName { get; set; }
	public string Email { get; set; }
	public string Phone { get; set; }

	public DateTime? FromDateOfLastOrder { get; set; }
	public DateTime? ToDateOfLastOrder { get; set; }
}

To be able to access Advanced search fields in View through a model, I passed CustomerSearchForm as generic when declaring grid:



public class CustomerController : Controller
{
        private Grid<Customer, CustomerSearchForm> _grid;
        ...
}

public class Grid<TEntity, TSearchForm> where  TSearchForm : SearchForm, new()
{
	private Pager _pager;
	private Sorter _sorter;
	private TSearchForm _searchForm;
    
   

This way I could use:

<td>
	First Name<br />
	<%= Html.TextBox("SearchForm.FirstName", null, new { @style = "width: 100px", @maxlength = "20" })%>
</td>

otherwise binder had issue binding to correct properties.

Then I added some animation with help of JQuery – to switch between Keyword and Advanced searches. See more about this in “Observations” section.

2. AJAX for Grid Actions
This functionality was very easy to implement in Web Forms. You just had to put UpdatePanel around the GridView and all paging and sorting would be asynchronous.

In MVC, it is a little more complicated, but with a benefit of a more control and flexibility. I moved Grid html (without search form) into a partial class “_Grid.aspx”. Then I used JQuery AJAX functionality and BlockUI plugin to perform all page actions without refreshing the whole page.


jQuery(document).ready(function() {

	initGrid();

 	//Intercept form submit and do submission with AJAX
	$("#grid").parents("form:first").submit(function() {
		submitFormWithAjax();
		return false;
	});
...
}); 


function submitFormWithAjax() {

	var form = $("#grid").parents("form:first");
	var formData= form.serialize();         //Serialize form control values to be passed asynchronously

	block();   //use BlockUI to display "Processing ... " popup

	$.ajax({
		type: "POST",
		url: "_Grid",           //Partial view
		data: formData,
		success: function(newHtml) {
			//Use timeout of half a second to see "Processing..." animation, otherwise it goes by too quickly.
			setTimeout(function() {
							$("#grid").html(newHtml);
	
							initGrid();
							unblock(); }, 500);
		},

		error: function(request, textStatus, errorThrown) {
			alert("AJAX error: " + request.statusText);
			unblock();
		}
	});
}

function block() {
	$('#grid #grid-data').block({
		message: '<div class="blockUI-message">Processing...</div>',
		css: { border: 'none', width: 'auto' },
		overlayCSS: { backgroundColor: '#C0C0C0' }
	});
}

function unblock() {
	$('#grid #grid-data').unblock();
}

3. Support for users with javascript disabled
This feature allows users with Javascript disabled to use most of the Grid’s functionality. Surprisingly, there are still 5% of users who have JS disabled for variety of reasons (http://www.w3schools.com/browsers/browsers_stats.asp).

For Paging and Sorting, I previously submitted the form using javascript, so the links would look like:

//paging
<a href="#" onclick="goToPage(2)">&gt;</a> 


//sorting
<th>
	<a href="#" class="" onclick="sort('FirstName', 'Asc');">First Name</a>
</th>

//with javascript used to set appropriate hidden variables and grid action, then submit the form
function goToPage(pageIndex) {
	$("#Pager_CurrentPage").val(pageIndex);
	$("#GridAction").val("GoToPage");

	submitForm();
}

function sort(sortField, sortDirection) {
	$("#GridAction").val("Sort");
	
	$("#Sorter_SortField").val(sortField);
	$("#Sorter_SortDirection").val(sortDirection);

	submitForm();
}

For this to work with JS disabled, i replaced “#” in href attribute with a url containing the whole grid state as well as appropriate paging/sorting action. So now, paging and sorting links look like:


//Paging
<a href="/Customer/List?Pager.CurrentPage=2&Pager.PageSize=5&Sorter.SortField=ID&Sorter.SortDirection=Asc&SearchForm.IsAdvanced=False&SearchForm.Keyword=&GridAction=GoToPage" onclick="goToPage(2); return false;">&gt;</a>

//Sorting
<a class="" href="/Customer/List?Pager.CurrentPage=1&Pager.PageSize=5&Sorter.SortField=FirstName&Sorter.SortDirection=Asc&SearchForm.IsAdvanced=False&SearchForm.Keyword=&GridAction=Sort" onclick="sort('FirstName', 'Asc'); return false;">First Name</a>

Notice that onclick now has a “return false;” statement. This suppresses default link action – which is to go to URL specified in HREF.

Internally this is implemented using Grid.GetQueryString and Grid.GenUrl methods, that are called from methods used to generate links – Grid.PageNavActionLink(…) and Grid.SortActionLink(…).

One nice side-effect from using this approach is that a Search Engine crawler like Google crawler should be able to crawl all the data in the grid using page/sort URLs.

For Page Size, I added “Refresh” button which is only displayed if JS is disabled:

<noscript><input type="submit" id="refresh-button" value="refresh" /></noscript>

One feature that doesn’t work yet for “JS disabled” mode is switching Search to Advanced Form. I will get it to work in next part.

4. Separate grid.js and grid.css
It is usually a bad practice to keep big chunks of javascript and CSS inline on the actual page. To correct this issue, I moved all grid javascript and styles into separate files.

JS is now in ‘/Scripts/grid.js’.
CSS is now in ‘/Content/grid.css’.

This will also help with re-usability.

Observations

  • When adding conditions for Advanced search I discovered that C# String.Contains method doesn’t have an option for case-insensitive comparison. To do that I added string extensions from the following link:
    http://schleichermann.wordpress.com/2009/02/24/c-stringcontains-case-insensitive-extension-method/

    public static class StringExtensions
    {
    	public static bool Contains(this String source, String value, StringComparison comparison)
    	{
    		return source.IndexOf(value, comparison) != -1;
    	}
        
    	public static bool ContainsCaseInsensitive(this String source, String value)
    	{
    		return Contains(source, value, StringComparison.CurrentCultureIgnoreCase);
    	}
    }
    
  • When I worked on animation for hiding/showing advanced search, I noticed that animations ran asynchronously, so sometimes it would fully skip the first animation if the second animation was called right away.

    Originally the code looked like this:

    $("#search-form #advanced-link").click(function() {
    
    	//Hide keyword search form using slide up animation
    	$("#search-form").slideUp("medium");
    
    	//Hide/show appropriate links and form fields
    	...
    	
    	//Display advanced search form using slide down animation
    	$("#search-form").slideDown("medium");
    
    	...
    });
    

    Since slideUp was asynchronous, slideDown would get called even before slideUp would get started, so all I would see was the Keyword form disappearing (without animation) and Advanced search appearing with animation.

    To make sure that slideUp animation finished before calling slideDown – I changed the code to call slideDown in slideUp’s callback argument. Final code looks like this:

    $("#search-form #advanced-link").click(function() {
    	$("#search-form").slideUp("medium", function() {
    		...
    		$("#search-form").slideDown("medium");
    	});
    
    	...
    });
    

Coming up…

Next will be the biggest task of them all. Making grid into re-usable control…

July 10, 2009

ASP.NET MVC Grid – Part 5 – Persisting Grid State

Filed under: ASP.NET MVC,General — Eric P @ 8:14 am

In this part I will implement the following items:

  • persist the state of the grid
  • update UI with row striping and row click actions
  • handle a “last customer on page” edge case
  • add status/notifications line when customer is added/deleted/modified
  • validate edit form
Click on the image to view the whole screenshot.

Click on the image to view the whole screenshot.

You can see demo here:
http://samples.entechsolutions.com/MvcGridSample/Part5

Code is available here:
http://samples.entechsolutions.com/Downloads/MvcGridSample/MvcGridSample_Part5.zip

Implementation

1. Persist state of the grid
A common use case for any grid control – is remembering the state of the grid when user comes back to the list page. So if grid is currently on page 2 and sorted by phone and user clicks on Edit – then saves customer and goes back to grid – the grid will still be on page 2 and sorted by phone.

To implement this functionality I created a new service class GridStateService with methods
Save(key, Grid)
Grid Load(key)
bool Exists(key);

This service uses session to save/load grid’s state, so while the person is using the site and his/her session hasn’t expired, the grid state (current page, page size, etc…) persists. If needed, it should be simple to change the persistence mechanism to store grid state in the cookie or the database. In those cases, whenever user would come back to the site, his/her last preferences (like sorting by “Phone” or page-size=50) would remain.

Here is how GridStateService is used in CustomerController.List:


[AcceptVerbs(HttpVerbs.Get)]
public ActionResult List()
{
	string key = GetGridKey();   //This function returns current url used to uniquely identify the grid
	if (_gridStateService.Exists(key))
	{
		_grid = _gridStateService.Load(key);
	}
	else
	{
		_grid = new Grid<Customer>
					(
						new Pager {CurrentPage = 1, PageSize = 5}, 
						new Sorter("ID", SortDirection.Asc)
					);
	}

	UpdateGridData();

	return View(_grid);
}

Here is the code the saves last state of the grid.


[AcceptVerbs(HttpVerbs.Post)]
public ActionResult List(Grid<Customer> grid)
{
	_grid = grid;

	_grid.ProcessAction();

	UpdateGridData();

        //Check if there is data in the grid
	if (_grid.Data.Count() > 0)
		SaveGridState();
	
	return View(_grid);
}

private void SaveGridState()
{
	string key = GetGridKey();
	_gridStateService.Save(key, _grid);
}

2.Row striping and row click

The row striping and highlighting (which allows for better readability) was implemented using approach described in the following article:
http://webdevdotnet.blogspot.com/2009/06/aspnet-mvc-jquery-part-2-zebra-striping.html

As for row-clicking – I added onlcick handler to data row’s tr tag:

<tr onclick="onRowClick(<%= item.ID %>)">
     <td>

When user clicks on the row they will be taken to customer edit screen. This is handled by JS function:

function onRowClick(id) {
	document.location = "/Customer/Edit/" + id;
}

3. Edge case for deleting last customer on the page

If I am using 5 rows per page with 6 customers and I deleted sixth customer on the second page, the grid should automatically switch to page 1 instead of showing page 2 with no customers.

This condition was handled in pager using the following code:

//handle a case when user deleted all rows on last page
if (_currentPage > _totalPages)
    _currentPage = _totalPages;

4. Status line when customer is added/deleted/modified

This functionality is related to operations triggered in the grid page.
Previously, if I deleted a customer there would be no indication that customer was deleted. The grid would get refreshed, but there would be no explicit notification that customer was actually deleted.

I used TempData functionality on the MasterPage to display such messages/notifications. All operations, add/edit/delete, that are successfully performed on the customer are now followed by a status line message (yellow box) on the grid page.
This message fades out after 5 seconds (ain’t JQuery great).

5. Validate add/edit form
This functionality is not really grid-related, but I thought it would be important to have it for sample completeness. It is not very realistic to add a customer with all fields empty. It will also help when I implement inline editing.

I used Data Annonations approach described here:
http://www.asp.net/learn/mvc/tutorial-39-cs.aspx

I update UI a little to display error message right next to the input fields which are invalid.

Observations

  • Data annotations has a property DataType which takes DataType enumeation consisting of members like “Currency”, “Email Address”, etc… I originally thought it would validate my fields according to the type I pass, but that doesn’t work. But it doesn’t work this way. In one of the comments to article http://www.aspworkshops.com/blog/archive/2008/09/10/asp-net-mvc-tip-43-use-data-annotation-validators.aspx it states:

    The DataType attribute are not validators.
    So DataType(DataType.Email) will not validate a string for being an email address. These are ui type hint sttributes.

    GRRRRRRReat… I guess I will need a reg expression validator or custom validator for Email/Phone validations.

  • When messing with Data Annotations, I discovered another error. Even though it worked ok when binding simple models, I started getting NullReferenceException when binding to complex models (with sub objects). I discovered the fix here:

    http://stackoverflow.com/questions/820468/how-does-dataannotationsmodelbinder-work-with-custom-viewmodels

  • One more thing related to validation. Looks like there is no way to provide custom error messages for data type conversions during binding. So if user enters some text into “Orders Placed” field – the error message is always:

    The value ‘SOME VALUE’ is not valid for the Orders Placed field.

    This is confirmed by Scott Gu in one of the comments to the following article:
    http://weblogs.asp.net/scottgu/archive/2008/09/02/asp-net-mvc-preview-5-and-form-posting-scenarios.aspx

    # re: ASP.NET MVC Preview 5 and Form Posting Scenarios
    Wednesday, September 03, 2008 11:39 PM by ScottGu

    Hi Florian,

    >>>>>>> I very much like the approach, cannot wait until this stuff is baked. One question – is there a way to intercept input validation and specify custom ErrorMessages?

    >>>>>>> E.g. in your example where the user enters a random string instead of the decimal number required by the model field you’re auto-generating an error message (‘.. invalid value ..’).

    >>>>>>> Is there a way to customize (and localize) this message, without inspecting and changing ModelState explicitely?

    Unfortunately with Preview 5 you need to manipulate the ModelState dictionary directly to enable this (either that or override the ModelBinder behavior to customize your own message).

    Hope this helps,

    Scott

    NOTE TO SELF: Add custom binder with a way to override type conversion error message.

Coming up…

There are a couple of items left before I will make the grid generic and re-usable. For the next part I will work on:

  • Making grid work if Javascript is disabled. This was brought to my attention by a comment in Part 3. Thank you, sironfoot.
  • Advandced search form – which will include fields “First Name”, “Last Name”, “Email”, date range for “Date of Last Order”, etc…
  • AJAX implementation so the whole page doesn’t refresh. Will work similar to how GridView worked in ASP.NET web forms if you put UpdatePanel around it.

July 6, 2009

ASP.NET MVC Grid – Part 4 – View Models, Unit Testing, Keyword Search

Filed under: ASP.NET MVC,General — Eric P @ 12:00 am

In this part, I decided to take a step back and do some refactoring. At the same time I would like to keep moving forward, by adding new Keyword Search functionality as well as an Auto Complete control to quickly find/edit a customer.

Click image to view full screenshot

Click image to view full screenshot

You can see demo here:
http://samples.entechsolutions.com/MvcGridSample/Part4

Code is available here:
http://samples.entechsolutions.com/Downloads/MvcGridSample/MvcGridSample_Part4.zip

Implementation

1. Use View Model (instead of ViewData) to organize Grid variables

As I mentioned in last couple of posts, ViewData was being used for too many variables. Such variables in ViewData are not strongly typed and result in verbose code which could be error-prone (for example null errors).

Instead I introduce the following class structure for Grid View Model:

class Grid
{
	Pager
		CurrentPage
		TotalPages
		RowStats
		...

	Sorter 
		SortField
		SortDirection


	SearchCriteria	- new functionality for keyword search
		Keyword


	GridAction - enumeration with items like "Sort", "GoToPage", ...
}

Using this class feels a lot cleaner then ViewData. In the code below Model is of class Grid.

<div id="gridHeader">
			<table>
				<tr>
					<td id="rowStats">
						<%= Model.Pager.RowStats %>
					</td>
					
					<td id="pagerNav">			
						<% if (Model.Pager.IsNavVisible) { %>
							<table>
								<tr>
									<td>
										<% if (Model.Pager.IsFirstPage) { %>
											<span class="disabled">&lt;&lt;</span>
										<% } else { %>
											<a href="#" onclick="goToPage(<%= Model.Pager.FirstPage %>)">&lt;&lt;</a>  
										<% } %>
									</td>
...

2. Write unit tests for pager. When creating a Pager class, I decided to follow TDD methodology and wrote unit tests first. Through this approach I discovered a lot of errors before actually integrating Pager into UI.

Here is a couple of unit tests… That check cases when Pager navigation should be visible and not visible.

namespace MvcGridSample.Tests.ViewModels.Shared
{
	[TestClass]
	public class PagerTest
	{

	       [TestMethod]
		public void Nav_Not_Visible_If_One_Page()
		{
			var pager = CreateAndInitPager(1, 10, 5);
			Assert.IsFalse(pager.IsNavVisible);
		}


		[TestMethod]
		public void Nav_Visible_If_More_Then_One_Page()
		{
			var pager = CreateAndInitPager(1, 10, 15);
			Assert.IsTrue(pager.IsNavVisible);

		}
               ...

3. Add Keyword search. This functionality allows user to search data by keyword which is matched against First Name, Last Name, Email or Phone. It can be expanded to search through other fields too (for ex. Customer Address – if one would exist).

The keyword search criteria was added to LINQ query using the following code:

private IQueryable<Customer> AddQuerySearchCriteria(IQueryable<Customer> query, SearchCriteria searchCriteria)
{
	if (!String.IsNullOrEmpty(searchCriteria.Keyword))
	{
		string keyword = searchCriteria.Keyword.ToLower();
		query = query.Where(customer => customer.FirstName.ToLower().Contains(keyword)
		                                || customer.LastName.ToLower().Contains(keyword)
						|| (customer.Email != null && customer.Email.Contains(keyword))
						|| (customer.Phone != null &&  customer.Phone.Contains(keyword)));
	}

	return query;
}

Note that I had to ensure that Email and Phone were not null, before checking them against keyword. Otherwise there were some “object reference is null” exceptions.

4. Add Auto Complete on Keyword. This is for a common use case when you would like to quickly find a record and perform certain operation on it. To try it out – start typing name of existing customer in keyword box. After first couple of letters you will see auto-complete drop down with a list of matching customers (by first name and last name).

If you select the customer in the list and click enter or click on it with a mouse – you will be taken to Edit Customer page.

To implement this functionality I used a JQuery auto-complete plugin from here:
http://docs.jquery.com/Plugins/Autocomplete

For this to work I created a method on controller that would return list of customer names in JSON format:

public ActionResult GetKeywordAutoCompleteData(string q, int limit)
{
	string keyword = q;

	IQueryable<Customer> query = _customerService.GetQueryable();
	query = query.Where(customer => customer.FirstName.ToLower().StartsWith(keyword)
	                                || customer.LastName.ToLower().StartsWith(keyword))
					.Take(10)
					.OrderBy(customer => customer.FirstName)
					.ThenBy(customer => customer.LastName);

	var list = query.Select(customer => new { 
												customer.ID, 
												Name = customer.FirstName + " " + customer.LastName});

	return Json(list);
}

Then call this method from client side using JQuery auto-complete plugin functionality:

jQuery(document).ready(function() {

	$("#SearchCriteria_Keyword").autocomplete(
		"/Customer/GetKeywordAutoCompleteData",
        {
        	dataType: 'json',
        	parse: function(data) {
        		var rows = new Array();

        		for (var i = 0; i < data.length; i++) {
        			rows&#91;i&#93; = { data: data&#91;i&#93;, value: data&#91;i&#93;.Name, result: data&#91;i&#93;.Name };
        		}
        		return rows;
        	},
        	formatItem: function(row, i, n) {
        		return row.Name;
        	},
        	width: 260,
        	selectFirst: false
        }
    );
    ....
&#91;/sourcecode&#93;


<h3>Observations</h3>
<ul>

<li>
         When HtmlHelper functions generate HTML controls - control IDs replace character  "." with "_".  For ex, if I want to use the following control in Jquery:


     <%= Html.Hidden("Sorter.SortDirection")%>

I need to call it like this:

     $("#Sorter_SortDirection").val(someVal);
  • Coming from Web Forms world, it seemed natural to me to create /App_Code directory to put all the utility classes.
    It has been working fine till now, but all of a sudden I started receiving compilation error:

    Compiler Error Message: CS0433: The type ‘MvcGridSample.StringFormatter’ exists in both ‘c:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\752a9979\7f1c3889\assembly\dl3\da179bc1\70581e2e_8ff9c901\MvcGridSample.DLL’ and ‘c:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\752a9979\7f1c3889\App_Code.nzexrw9-.dll’

    The reason for this issue is explained on the following page:
    http://weblogs.asp.net/meligy/archive/2008/08/03/converting-vs-2008-website-to-web-application.aspx

    During the conversion, VS 2008 renames your “App_Code” to “Old_App_Code”. This new name sounds ugly, but DO NOT RENAME IT BACK. In the “web application” model, all code will be in one assembly. In runtime, the web server does not know what web project type you are using. It does take all code in “App_Code” folder and create a new assembly for it. This way, if you have code in folder named “App_Code”, you’ll end up with RUNTIME compilation errors that the same types exist in two assemblies, the one created by VS, and the one created by IIS / ASP.NET Development Server.

    So to fix the issue I renamed the folder to “App_Code_”

  • Coming up for part 5…

    In part 5, I will implement grid persistance. Currently, if I am on page 3 of the grid sorted by phone number and I click on “Edit” next to one of the customers – when I save the customer and go back to grid – the grid returns to its default state of Page 1 and sorted by ID.

    Grid persistence – will remember the state of the grid when user goes to another page, so that when user will come back to the page with the grid, it will be in the state that it was left in.

    I will also add more JQuery functionality to format alternative rows, change row color on hover and provide “status line” notifications of user actions like created new customer, deleted customer, etc….

    Next Page »

    Create a free website or blog at WordPress.com.