sâmbătă, 17 martie 2012

asp mvc internationalization

http://afana.me/post/aspnet-mvc-internationalization-part-2.aspx


In my previous blog post, ASP.NET MVC 3 Internationalization, I talked in detail about creating a multilingual ASP.NET MVC 3 website and showed the different techniques that can be used to globalize an MVC 3 website. Perhaps the best way to learn about internationalization is by applying it to a real-world application. In this post, I am going to show you how to globalize the popular web application, NerdDinner. We’ll tackle the obstacles one by one and from server-side to client-side. We will add the Spanish language to the website, so we can have Spanish nerds joining us.

Introduction

If a website targets users from different parts of the world, these users might like to see the website content in their own language. Creating a multilingual website is not an easy task, but it will certainly allow a web site to reach more audience. Fortunately, the .NET Framework already has components that support different languages and cultures.
We will revamp the NerdDinner web application, so that:
  • It can display contents in different languages.
  • It autodetects the language from the user's browser.
  • It allows the user to override the language of their browser.
Internationalization, Globalization and Localization. What confusion!
It is actually confusion. The terms defined by Microsoft are different from the rest of the industry.
In Microsoft terms, Internationalization involves Globalization and Localization. Globalization is the process of designing applications that support different cultures. Localization is the process of customizing an application for a given culture. Other industries, however, transpose the meanings of Internationalization and Globalization. You can read more about this here.

Internationalization or Globalization?

In this post, it makes sense to use Microsoft’s terminology since both .NET and ASP.NET MVC 3 are Microsoft’s products.
Anyway, Internationalization is often abbreviated to "I18N". The abbreviation takes the first and last letters and the number of letters between them, so 18 stands for the number of letters between the first "I" and the last "N". The same applies to Globalization (G11N), and Localization (L10N).
A culture is a language and, optionally, a region. The format for the culture name is "<languagecode2>-<country/regioncode2>", where <languagecode2> is the language code and <country/regioncode2> is the subculture code. Examples include "es-CL" for Spanish (Chile) and "en-US" for English (United States). A complete list can be found here. When a culture has a specified language, but not a region, we call it a neutral culture. A neutral culture is not specific to any region. For example, "en" represents English, but not English of the United States.
Now, let’s review the terms used so far:
  • Globalization (G11N): The process of making an application support different languages and regions.
  • Localization (L10N): The process of customizing an application for a given language and region.
  • Internationalization (I18N): Describes both globalization and localization.
  • Culture: It is a language and, optionally, a region.
  • Locale: A locale is the same as a culture.
  • Neutral culture: A culture that has a specified language, but not a region. (e.g. "en", "es")
  • Specific culture: A culture that has a specified language and region. (e.g. "en-US", "en-GB", "es-CL")

Why do we need a region? Isn’t a language alone enough?

You might not need a region at all. It is true that English in the United States is not the same as English in the United Kingdom but if your application just shows English text readable to people from these English-speaking countries, you will not need a region. The problem arises when you need to deal with numbers, dates, and currencies. For example, compare the following output for two different Spanish-speaking regions (Chile, Mexico):
int value = 5600;
Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("es-CL");

Console.WriteLine(DateTime.Now.ToShortDateString());
Console.WriteLine(value.ToString("c"));

Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("es-MX");

Console.WriteLine(DateTime.Now.ToShortDateString());
Console.WriteLine(value.ToString("c"));

// Output
26-07-2011 // Date in es-CL, Spanish (Chile)
$ 5.600,00 // Currency in es-CL, Spanish (Chile)

26/07/2011 // Date in es-MX, Spanish (Mexico)
$5,600.00 // Currency in es-MX, Spanish (Mexico)
You can notice the difference in date and currency format. The decimal separator in each region is different and can confuse people in the other region. If a Mexican user types one thousand in their culture "1,000", it will be interpreted as 1 (one) in a Chilean culture website. We mainly need regions for this type of reasons and not much for the language itself.

Culture and UICulture

ASP.NET keeps track of two culture values, the Culture and UICulture. The culture value determines the results of culture-dependent functions, such as the date, number, and currency formatting. The UICulture determines which resources are to be loaded for the page by the ResourceManager. The ResourceManager simply looks up culture-specific resources that is determined by CurrentUICulture. Every thread in .NET has CurrentCulture and CurrentUICulture objects. So ASP.NET inspects these values when rendering culture-dependent functions. For example, if current thread's culture (CurrentCulture) is set to "en-US" (English, United States), DateTime.Now.ToLongDateString() shows "Saturday, January 08, 2011", but if CurrentCulture is set to "es-CL" (Spanish, Chile) the result will be "sábado, 08 de enero de 2011".

How can ASP.NET guess the user’s language?

On each HTTP request, there is a header field called Accept-Language which determines which languages the user’s browser supports:
Accept-Language: en-us,en;q=0.5
This means that my browser prefers English (United States), but it can accept other types of English. The "q" parameter indicates an estimate of the user’s preference for that language. You can control the list of languages using your web browser.
 
Internet Explorer
 
Firefox

How to Support Different Cultures in ASP.NET MVC 3

There are two ways to incorporate different cultures in ASP.NET MVC 3:
  • By using resource strings in all our site views.
  • By using different set of views for every culture.
  • By mixing between 1 and 2

Which one is the best?

It is a matter of convenience. Some people prefer to use a single view for all languages because it is more maintainable. While others think replacing views content with code like "@Resources.Something" might clutter the views and will become unreadable. Some project requirements force developers to implement different views per language. But sometimes you have no choice where layout has to be different like right-to-left languages. Even if you set dir="rtl", this won’t be enough in real applications unless the project’s UI layout is really simple. Perhaps, a mix of the two is the best. Anyway, for this example, it makes sense to use resources since we won’t have any issue with the layout for the Spanish and English languages that we will use.

Internationalizing NerdDinner

Download the NerdDinner project from CodePlex site and extract the VS2010-MVC3-Razor folder. We will do the following steps in order to convert NerdDinner into a multilingual website:
  1. Resources: We’ll create resources files for each culture.
  2. Views: All text in the views must be extracted and added in the resource files instead.
  3. Source code: All text in C# code files must be extracted and added in the resource files, too.
  4. Model and Validation Rules: Validation rules messages have to be moved to Resources.
  5. Create a Culture Helper and a Base Controller.
  6. Client-Side: All client side code must be localized including dates, numbers, etc.

Resource files

We will store resource files in a separate assembly, so we can reference them in other project types in the future.
Right click on the Solution and then choose the "Add->New Project" context menu command. Choose "Class Library" project type and name it "Resources".
Now right click on "Resources" project and then choose "Add->New Item" context menu command. Choose "Resource File" and name it "Resources.resx". This will be our default culture (en-US) since it has no special endings. A default culture means that when the user asks for a language that we did not implement, they will see the English version.
We want to enable Spanish, but which Spanish? Or rather, which region?
We certainly want to have nerds from all Spanish-speaking countries including Argentina and Peru, and not only from a specific country. In this case, we care about the language, not the region, so we don’t care about the country in the translation strings. We don’t need to create a resource file for every culture "es-CL", "es-MX", etc. We just need one common file.
Now create a new resource file and name it "Resources.es.resx" for the Spanish version of the site.
Before we move one, make sure to mark the resource's access modifier property to "public" in both files, so it will be accessible from other projects.

Views

We need to extract the English text from all the views and move it to the resource files. This is going to take a while as we have to do this for every single view. You can download the whole NerdDinner project internationalized from the link at the top of this page.
Now we need to reference "Resources" project from our web application, so that we can read the resource strings right from our web site. Right click on "References" under our web project "NerdDinner", and choose the "Resources" project from Projects tab.
Here is a trick, instead of typing the namespace name each time (e.g. "@Resources. Resources.LogOn "), we can add this namespace to the views Web.config and type "@Resources.LogOn" instead. Open the Web.config file under the views folder.
You also get IntelliSense when accessing these resource strings since Visual Studio generates a class behind the scenes that contains a typed property for each key. Now we need to populate the resource files. Visual Studio has a nice resource editor that makes it easy to manage our translated strings.
Let "_LoginStatus.cshtml" be our first view. We need to extract "Log On", "Log Off", and "Welcome". That’s all what we need from this view.

English

Spanish
We have to repeat these steps for all other views in the project. Remember that link names must be translated like in this case. Everything that appears to the end user must be translated, but element names, ids should never be translated. In this project, I made the effort to do the translation myself, but normally you as a developer will receive all the text already translated.
Note: There is no need to show all views in this post since they are done in a very similar way.

Validation Rules Internationalization

It is not surprising that controllers and other source code files contain text that needs to be translated, such as error messages sent to the end user. In order to globalize validation messages, we need to specify a few extra parameters. The "ErrorMessageResourceType" indicates the type of resource to look up the error message. "ErrorMessageResourceName" indicates the resource name to lookup the error message. Resource manager will pick the correct resource file based on the current culture.
namespace NerdDinner.Models
{
    public class LogOnModel
    {
        [Required(ErrorMessageResourceName = "YouMustSpecifyUsername", ErrorMessageResourceType = typeof(Resources.Resources))]
        [Display(Name = "ModelUsername", ResourceType=typeof(Resources.Resources)) ]        
        public string UserName { get; set; }

        [Required(ErrorMessageResourceName = "YouMustSpecifyPassword", ErrorMessageResourceType = typeof(Resources.Resources))]
        [DataType(DataType.Password)]
        [Display(Name = "ModelPassword", ResourceType = typeof(Resources.Resources))]
        public string Password { get; set; }

        [Display(Name = "ModelRememberMe", ResourceType = typeof(Resources.Resources))]
        public bool RememberMe { get; set; }
    }

    public class RegisterModel
    {
        [Required(ErrorMessageResourceName = "YouMustSpecifyUsername", ErrorMessageResourceType = typeof(Resources.Resources))]
        [Display(Name = "ModelUsername", ResourceType = typeof(Resources.Resources))]   
        public string UserName { get; set; }

        [Required(ErrorMessageResourceName = "YouMustSpecifyEmailAddress", ErrorMessageResourceType = typeof(Resources.Resources))]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "ModelEmail", ResourceType=typeof(Resources.Resources)) ]                
        public string Email { get; set; }

        [Required(ErrorMessageResourceName = "YouMustSpecifyPassword", ErrorMessageResourceType = typeof(Resources.Resources))]
        [StringLength(100, ErrorMessageResourceName = "PasswordTooShort", ErrorMessageResourceType = typeof(Resources.Resources), MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "ModelPassword", ResourceType = typeof(Resources.Resources))]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "ModelConfirmPassword", ResourceType = typeof(Resources.Resources))]
        [Compare("Password", ErrorMessageResourceName = "NewPasswordAndConfirmationMismatch", ErrorMessageResourceType = typeof(Resources.Resources))]
        public string ConfirmPassword { get; set; }
    }

}

Culture Helper and Base Controller

Before we move to Client-side, we need to figure out how to retrieve the culture from the user’s browser. I already mentioned there is a header field called "Accept-Language" that the browser sends on every request. This field contains a list of culture names (language-country) that the user has configured in their browser. The problem is that this culture may not reflect the real user's preferred language, such as a computer in a cybercafé. We should allow the user to choose a language explicitly and allow them to change it. In order to do this sort of things, we need to store the user's preferred language in a store, which can be perfectly a cookie.
We will create a base controller that inspects the cookie contents first, if there is no cookie, we will use the "Accept-Language" field sent by their browser. Create a controller and name it "BaseController" like below:
namespace NerdDinner.Controllers
{
    public class BaseController : Controller
    {
        protected override void ExecuteCore()
        {
            string cultureName = null;
            // Attempt to read the culture cookie from Request
            HttpCookie cultureCookie = Request.Cookies["_culture"];
            if (cultureCookie != null)
                cultureName = cultureCookie.Value;
            else
                cultureName = Request.UserLanguages[0]; // obtain it from HTTP header AcceptLanguages

            // Validate culture name
            cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe


            // Modify current thread's cultures            
            Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(cultureName);
            Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;

            base.ExecuteCore();
        }

    }
}
Make sure all controllers in this project inherit from this BaseController.
The base controller checks if the cookie exists, and sets the current thread cultures to that cookie value. Of course, because cookie content can be manipulated on the client side, we should always validate its value using a helper class called "CultureHelper". If the culture name is not valid, the helper class returns the default culture.

CultureHelper Class

The CultureHelper is basically a utility that allows us to store culture names we are implementing in the web site:
namespace NerdDinner.Helpers
{
    public static class CultureHelper
    {
        // Valid cultures
        private static readonly IList<string> _validCultures = new List<string> {"af", "af-ZA", "sq", "sq-AL", "gsw-FR", "am-ET", "ar", "ar-DZ", "ar-BH", "ar-EG", "ar-IQ", "ar-JO", "ar-KW", "ar-LB", "ar-LY", "ar-MA", "ar-OM", "ar-QA", "ar-SA", "ar-SY", "ar-TN", "ar-AE", "ar-YE", "hy", "hy-AM", "as-IN", "az", "az-Cyrl-AZ", "az-Latn-AZ", "ba-RU", "eu", "eu-ES", "be", "be-BY", "bn-BD", "bn-IN", "bs-Cyrl-BA", "bs-Latn-BA", "br-FR", "bg", "bg-BG", "ca", "ca-ES", "zh-HK", "zh-MO", "zh-CN", "zh-Hans", "zh-SG", "zh-TW", "zh-Hant", "co-FR", "hr", "hr-HR", "hr-BA", "cs", "cs-CZ", "da", "da-DK", "prs-AF", "div", "div-MV", "nl", "nl-BE", "nl-NL", "en", "en-AU", "en-BZ", "en-CA", "en-029", "en-IN", "en-IE", "en-JM", "en-MY", "en-NZ", "en-PH", "en-SG", "en-ZA", "en-TT", "en-GB", "en-US", "en-ZW", "et", "et-EE", "fo", "fo-FO", "fil-PH", "fi", "fi-FI", "fr", "fr-BE", "fr-CA", "fr-FR", "fr-LU", "fr-MC", "fr-CH", "fy-NL", "gl", "gl-ES", "ka", "ka-GE", "de", "de-AT", "de-DE", "de-LI", "de-LU", "de-CH", "el", "el-GR", "kl-GL", "gu", "gu-IN", "ha-Latn-NG", "he", "he-IL", "hi", "hi-IN", "hu", "hu-HU", "is", "is-IS", "ig-NG", "id", "id-ID", "iu-Latn-CA", "iu-Cans-CA", "ga-IE", "xh-ZA", "zu-ZA", "it", "it-IT", "it-CH", "ja", "ja-JP", "kn", "kn-IN", "kk", "kk-KZ", "km-KH", "qut-GT", "rw-RW", "sw", "sw-KE", "kok", "kok-IN", "ko", "ko-KR", "ky", "ky-KG", "lo-LA", "lv", "lv-LV", "lt", "lt-LT", "wee-DE", "lb-LU", "mk", "mk-MK", "ms", "ms-BN", "ms-MY", "ml-IN", "mt-MT", "mi-NZ", "arn-CL", "mr", "mr-IN", "moh-CA", "mn", "mn-MN", "mn-Mong-CN", "ne-NP", "no", "nb-NO", "nn-NO", "oc-FR", "or-IN", "ps-AF", "fa", "fa-IR", "pl", "pl-PL", "pt", "pt-BR", "pt-PT", "pa", "pa-IN", "quz-BO", "quz-EC", "quz-PE", "ro", "ro-RO", "rm-CH", "ru", "ru-RU", "smn-FI", "smj-NO", "smj-SE", "se-FI", "se-NO", "se-SE", "sms-FI", "sma-NO", "sma-SE", "sa", "sa-IN", "sr", "sr-Cyrl-BA", "sr-Cyrl-SP", "sr-Latn-BA", "sr-Latn-SP", "nso-ZA", "tn-ZA", "si-LK", "sk", "sk-SK", "sl", "sl-SI", "es", "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-SV", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PY", "es-PE", "es-PR", "es-ES", "es-US", "es-UY", "es-VE", "sv", "sv-FI", "sv-SE", "syr", "syr-SY", "tg-Cyrl-TJ", "tzm-Latn-DZ", "ta", "ta-IN", "tt", "tt-RU", "te", "te-IN", "th", "th-TH", "bo-CN", "tr", "tr-TR", "tk-TM", "ug-CN", "uk", "uk-UA", "wen-DE", "ur", "ur-PK", "uz", "uz-Cyrl-UZ", "uz-Latn-UZ", "vi", "vi-VN", "cy-GB", "wo-SN", "sah-RU", "ii-CN", "yo-NG" };

        // Include ONLY cultures you are implementing
        private static readonly IList<string> _cultures = new List<string> {
            "en-US",  // first culture is the DEFAULT
            "es", // Spanish NEUTRAL culture
            "es-AR", "es-BO", "es-CL", "es-CO", "es-CR", "es-DO", "es-EC", "es-SV", "es-GT", "es-HN", "es-MX", "es-NI", "es-PA", "es-PY", "es-PE", "es-PR", "es-ES", "es-US", "es-UY", "es-VE" // Specific cultures
           
        };



        /// <summary>
        /// Returns a valid culture name based on "name" parameter. If "name" is not valid, it returns the default culture "en-US"
        /// </summary>
        /// <param name="name">Culture's name (e.g. en-US)</param>
        public static string GetImplementedCulture(string name)
        {
            // make sure it's not null
            if (string.IsNullOrEmpty(name))
                return GetDefaultCulture(); // return Default culture

            // make sure it is a valid culture first
            if(_validCultures.Where( c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() == 0)
                return GetDefaultCulture(); // return Default culture if it is invalid

               
            // if it is implemented, accept it
            if (_cultures.Where( c => c.Equals(name, StringComparison.InvariantCultureIgnoreCase)).Count() > 0)
                return name; // accept it

           

            // Find a close match. For example, if you have "en-US" defined and the user requests "en-GB", 
            // the function will return closes match that is "en-US" because at least the language is the same (ie English)  
            var n = GetNeutralCulture(name);
            foreach (var c in _cultures)
                if (c.StartsWith(n))
                        return c; 
                


            // else 
            // It is not implemented
            return GetDefaultCulture(); // return Default culture as no match found
        }


        /// <summary>
        /// Returns default culture name which is the first name decalared (e.g. en-US)
        /// </summary>
        /// <returns></returns>
        public static string GetDefaultCulture()
        {
            return _cultures[0]; // return Default culture

        }

        public static string GetCurrentCulture()
        {
            return Thread.CurrentThread.CurrentCulture.Name;
        }

        public static string GetCurrentNeutralCulture()
        {
            return GetNeutralCulture(Thread.CurrentThread.CurrentCulture.Name);
        }
          

        public static string GetNeutralCulture(string name)
        {
            if (name.Length < 2)
                return name;

            return name.Substring(0, 2); // Read first two chars only. E.g. "en", "es"
        }

 
    


    }
}
We should populate "_cultures" manually. The "_cultures" dictionary stores the list of culture names our site supports. The nice part of this utility class is that it serves similar cultures. For example, if a user is visiting our site from the United Kingdom (en-GB), a culture which is not implemented in our site, he or she will see our site in English using "en-US" (English, United States). This way, you don't have to implement all cultures unless you really care about currency, date format, etc. One important thing to mention is that we added all specific cultures for the Spanish language. For example, we are implementing "es" Spanish in general without any region where a date looks like this "30/07/2011". Now suppose a nerd is coming from Chile (es-CL), it would be very nice display dates in their culture format (30-07-2011) instead of the neutral one. It does not matter if we don’t have a resource file for any of these cultures. ResourceManager can pick a neutral culture when it cannot find a specific culture file, this automatic mechanism is called fallback.

Output Caching

You might run into a problem with Output Caching as content varies by culture. If you think that VaryByHeader="Accept-Language" could do it, then this is not always the case because our culture is stored in a cookie, not in the header. If the cookie is absent, this approach will be OK. We overcome this by applying custom caching.
/* NerdDinner i18n Custom caching */
   public override string GetVaryByCustomString(HttpContext context, string arg)
        {
            // It seems this executes multiple times and early, so we need to extract language again from cookie.
            if (arg == "culture") // culture name (e.g. "en-US") is what should vary caching
            {
                string cultureName = null;
                // Attempt to read the culture cookie from Request
                HttpCookie cultureCookie = Request.Cookies["_culture"];
                if (cultureCookie != null)
                    cultureName = cultureCookie.Value;
                else
                    cultureName = Request.UserLanguages[0]; // obtain it from HTTP header AcceptLanguages

                // Validate culture name
                cultureName = CultureHelper.GetImplementedCulture(cultureName); // This is safe

                return cultureName.ToLower();// use culture name as cache key, "es", "en-us", "es-cl", etc.
            }
            
            return base.GetVaryByCustomString(context, arg);
        }
And it can be used this way:
[OutputCache(Duration=60, VaryByCustom="culture")]

Client Side

This is the last step in the process of i18n. Client side internationalization can be easy or challenging depending on the way you created your code and the tools you are using. The first thing to do is to replace all the strings inside javascript files with the translated versions. Then we need to figure out how to parse and format numbers, dates, and currency if any. We also need to localize the calendar used to set event dates.
Before we begin with the strings replacement, it is worth it to mention JQuery Global plugin. This jQuery plugin supports i18n for hundreds of cultures. It makes it easy to parse and format numbers, dates, and currencies. We will use this plugin in the NerdDinner project. Download the plugin and extract the folder "lib" contents in the "Scripts" folder of our project.
Add a reference to the "globalize.js" file in the "_Layout.cshtml". The jQuery Global plugin allows us to store culture specific values, so this can be the perfect place to store the translated strings. Of course, there are other ways to store localized strings inside javascript files. For example, we can create a separate javascript file for each culture, so that each file contains localized strings for that culture. However, it is simpler to store strings using this plugin.
Let's begin with NerdDinner.js file. Look at the following code snippet:
This method is fragile. It is appending an "s" to the end of the string. For English, this is fine. However, this won’t work in many other cultures, so we need two separate words "RSVP" and "RSVPs" stored separately. We can store these words in resource files and fetch them from there with this trick. There is also a confirmation message we need to replace. Here is how the code will look like:
Globalize.addCultureInfo("@NerdDinner.Helpers.CultureHelper.GetCurrentCulture()" /*es-CL*/, {
                    messages: {
                        "RSVP": "@Resources.RSVP",
                        "RSVPs": "@Resources.RSVPs",
                        "BingMapsReturnedAddress": "@Html.Raw(Resources.BingMapsReturnedAddress)",
                        "MonthDayYear": "@Html.Raw(Resources.MonthDayYear)",
                        "MonthDay": "@Html.Raw(Resources.MMMdd)",
                        "with":"@Resources.with"
                    }
                });
And now NerdDinner.js should look like below:
function _getRSVPMessage(RSVPCount)  {
           var rsvpMessage  =  ""  +  RSVPCount  +  " "  +  (RSVPCount  >  1  ?  Globalize.localize("RSVPs")  :  Globalize.localize("RSVP")); 
           return rsvpMessage; 
       }
    
And here is how you would replace the confirmation:
var answer  =  confirm(htmlDecode(Globalize.localize("BingMapsReturnedAddress").replace("{0}",  locations[0].Name).replace("{1}",  currentAddress.val())));
Now let’s move on to dates. Currently, the NerdDinner project is using a different jQuery plugin for date formatting. We need to replace it with the Global plugin.
Look at the following function:
function _getDinnerDate(dinner,  formatStr)  {
           return '<strong>'  +  _dateDeserialize(dinner.EventDate).format(formatStr)  +  '</strong>'; 
       }
        
Here is how it should be after using Global plugin:
function _getDinnerDate(dinner,  formatStr)  {
                  return '<strong>'  +  Globalize.format(_dateDeserialize(dinner.EventDate),  formatStr); 
     + '</strong>'; 
       }
        

The Calendar

There are two components that need to be localized: DatePicker and TimePicker. Luckily, both support i18n. DatePicker ships with javascript localization files, so we just need to reference the file of the implemented culture. The following references the right file from Microsoft CDN:
<script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.11/i18n/jquery.ui.datepicker-@(NerdDinner.Helpers.CultureHelper.GetCurrentNeutralCulture()).js" type="text/javascript"></script>
It is important to mention that the date picker plugin only supports neutral cultures (e.g. "en", "es"). This means we need to tweak its formatting.
The time picker plugin localization is done in the same way, but it doesn’t ship with its own localization file, so we need to do the process manually. Here is the complete js code for both components:
@if (NerdDinner.Helpers.CultureHelper.GetCurrentCulture() != "en" && NerdDinner.Helpers.CultureHelper.GetCurrentCulture() != "en-US")
    {
        // load necessary localization files for Global and DatePicker
         <script src="http://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.11/i18n/jquery.ui.datepicker-@(NerdDinner.Helpers.CultureHelper.GetCurrentNeutralCulture()).js" type="text/javascript"></script>
        
         <script type="text/javascript">
             $(function () {
                 
                 @* Unfortunately, the datepicker only supports Neutral cultures, so we need to adjust date and time format to the specific culture *@
                 $("#EventDate").change(function(){
                     $(this).val(Globalize.format($(this).datetimepicker('getDate'), Globalize.culture().calendar.patterns.d + " " + Globalize.culture().calendar.patterns.t)); /*d t*/
                });
                
                @* Timepicker i18n *@
                $.timepicker.regional['@NerdDinner.Helpers.CultureHelper.GetCurrentCulture()'] = {
                timeOnlyTitle: '@Resources.timeOnlyTitle',
                timeText: '@Resources.timeText',
                hourText: '@Resources.hourText',
                minuteText: '@Resources.minuteText',
                secondText: '@Resources.secondText',
                currentText: '@Resources.currentText',
                closeText: '@Resources.closeText',
                ampm: '@Resources.ampm'
            };

            $.timepicker.setDefaults($.timepicker.regional['@NerdDinner.Helpers.CultureHelper.GetCurrentCulture()']);

             });
         </script>
    }
DatePicker doesn’t support specific cultures like "es-CL". This will be an issue for ASP.NET. For example, the date value "30/07/2011" is not a valid date in the Chilean culture, which uses "-" (i.e. "30-07-2011"). ASP.NET will throw an exception when parsing a date in an incorrect format. To solve this issue, each time the user modifies the event date, we need to re-format it using Globalize.format method.
For TimePicker, we are providing the plugin the translated strings from the resource files. We are telling the plugin to use the Spanish culture like this:
$.timepicker.setDefaults($.timepicker.regional['@NerdDinner.Helpers.CultureHelper.GetCurrentCulture()']);

Now you can see the calendar in Spanish
Now that dates and time appear in correct format to the end user, let’s move to numbers parsing and formatting.
NOTE It seems that DefaultModelBinder is not culture-ware. It does not always use the current culture for type conversion. If the data value is coming as part of the URL, the InvariantCulture takes place. Otherwise, the current culture is used. This might be a big issue. Luckily, it’s not a problem in this case since we are not using any values as part of a URL. Anyway, the invariant culture is culture-insensitive, and it is similar to English but is not related to any region. Suppose a culture on the server is set to "es-CL" which expects dates in this format "30-07-2011" and an incoming querystring has the value "01-08-2011". An exception is thrown since DefaultModelBinder used the InvariantCulture which can parse dates in this format "mm/dd/yyyy" or "yyyy-mm-dd".

Numbers

Although the end user is not dealing with any numbers directly, there are two hidden input fields that actually store the Bing Maps pushpin coordinates. Since these values are of type float, they contain a decimal point which poses a problem across different cultures as explained earlier. To overcome this issue, each time these values are read or saved, we need to parse or format them. Otherwise, we’ll get a server-side error from ASP.NET. Anyway, we can use Globalize.parseFloat and Globalize.format for this purpose. Below is some code snippet from NerdDinner.js
if (NerdDinner._points.length  ===  1)  {
           $("#Latitude").val(Globalize.format(NerdDinner._points[0].Latitude,  "n14")); 
           $("#Longitude").val(Globalize.format(NerdDinner._points[0].Longitude,  "n14")); 
       }

Unobtrusive Client-Side Validation

Client side validation is important in nowadays web applications because users expect immediate response. It’s not a good thing to make the a user wait for the server load response just to tell them if "10" is a valid number. A lot of things can be done on the client side which can also boost performance by reducing the number of sever requests. Unobtrusive validation does not add any javascript in the views. Instead, the validation process is done separately. However, validation rules are described as attributes that are added to the HTML elements to be validated. These attributes are processed by some MVC client side library which configures jQuery Validation tool that does all the job behind the scenes.
Anyway, NerdDinner is using unobtrusive client-side validation, and jQuery validation by default does not consider i18n. We need to override some of the validation methods in order to have i18n. In our NerdDinner case, the only problem occurs when we try to place an event since the Latitude and Longitude input values can be misinterpreted in different cultures.
We can either turn off client-side validation for these inputs, or we can tell jQuery validation library to parse them correctly. We’ll choose the latter since in other applications, it may not be pleasant to turn off validation.
// Override default jQuery Validation number method
        $.validator.methods.number  =   function(value,  element)  {
               return  this.optional(element)  ||  !isNaN(Globalize.parseFloat(value)); 
             }
In this method, the return value is either true or false. We are checking if the result of Globalize.parseFloat is a valid number.

Language picker

Finally, we need to allow the user to choose their preferred language. We need a language selector. I already created a one for this purpose. Below is the code needed to add the language picker to the _Layout.cshtml view:
<div class="lang-picker-wrapper">
            <span class="lang-picker">
                 <a class="en-us" href="javascript:void(0);">English</a>
                 <a class="@(NerdDinner.Helpers.CultureHelper.GetNeutralCulture(Request.UserLanguages[0].ToLower()) == "es" ? Request.UserLanguages[0].ToLower() : "es")" href="javascript:void(0);">Español</a>
            </span>
            </div>
We used the AcceptLanguage header field here on purpose. Suppose a nerd is visiting our site from Spain, instead of using the neutral culture "es", we want to use the user’s specific culture "es-ES" instead. However, if the user’s browser does not prefer Spanish and they prefer to see our site in Spanish, the neutral culture is applied.
$(".lang-picker a." + Globalize.culture().name.toLowerCase()).prependTo($(".lang-picker")); @* Select current culture*@

                @* culture change *@
                $(".lang-picker a").click(function(){

                if ($(this).hasClass(Globalize.culture().name.toLowerCase()))
                    return false; // do nothing

                $.cookie("_culture", $(this).attr("class") , {expires : 365, path: '/'});
                window.location.reload(); // reload 

                });
And the javascript needed for this to work:
In the code above, we attach a click handler to each link, so the user can click on them to change their language. The culture name is stored in the "class" attribute of the link. When the language link is clicked, the cookie’s value is updated, and the page is reloaded to reflect the new culture.

Try It Out

Let’s see how NerdDinner looks like now:
Everything looks fine except for one thing, the image next to the search text box is still in English because it is not text. We have two options: Either we convert this image into text, or we create a different image file per language. I have already prepared a Spanish version of this image. It doesn’t look exactly the same, but it’s still good. We have to reference the right image file in CSS. However, we can’t use C# directly in CSS files, so we need to move the style into our view.
<style type="text/css">
        #hm-masthead
        {
            background: transparent url('Content/img/hm-masthead-@(NerdDinner.Helpers.CultureHelper.GetCurrentNeutralCulture()).png') no-repeat 0px 45px;
        }
    
    </style>
We used the neutral version of the culture because we only need one image per language, not per region.

Enjoy!!

Now we can have Spanish nerds joining us!!

joi, 9 februarie 2012

PLINQ

Parallel LINQ (PLINQ)
Parallel LINQ, also known as PLINQ, is a parallel implementation of LINQ to objects. PLINQ
implements all the LINQ query extension methods and has additional operators for parallel
operations. The degree of concurrency for PLINQ queries is based on the capabilities of the
computer running the query.
In many, but not all, scenarios, PLINQ can provide a significant increase in speed by using
all available CPUs or CPU cores. A PLINQ query can provide performance gains when
you have CPU-intensive operations that can be paralleled, or divided, across each CPU or
CPU core. The more computationally expensive the work is, the greater the opportunity for
performance gain. For example, if the workload takes 100 milliseconds to execute, a sequential
query over 400 elements will take 40 seconds to complete the work, whereas a parallel
query on a computer with eight cores might take only 5 seconds. This yields a speedup of 35
seconds.
One problem with Windows applications is that when you try to update a control
on your form from a thread other than the thread that created the control, an
InvalidOperationException
is thrown with the message, “Cross-thread operation not valid:
Control ‘txtLog’ accessed from a thread other than the thread it was created on.” To work with
threading, update in a thread-safe way the following extension method for TextBox to the
TextBoxHelper class.
Sample of Visual Basic Code
<Extension()> _
Public Sub WriteLine(ByVal txt As TextBox, _
ByVal format As String, _
ByVal ParamArray parms As Object())
Dim line As String = String.Format((format & Environment.NewLine), parms)
If txt.InvokeRequired Then
txt.BeginInvoke(New Action(Of String)(AddressOf txt.AppendText), _
New Object() {line})
Else
txt.AppendText(line)
224 CHAPTER 3 Introducing LINQ
End If
End Sub
Sample of C# Code
public static void WriteLine(this TextBox txt,
string format, params object[] parms)
{
string line = string.Format(format + Environment.NewLine, parms);
if (txt.InvokeRequired)
{
txt.BeginInvoke((Action<string>)txt.AppendText, line);
}
else
{
txt.AppendText(line);
}
}
You use the Invoke or BeginInvoke method on the TextBox class to marshal the callback to
the thread that was used to create the UI control. The BeginInvoke method posts an internal
dedicated Windows message to the UI thread message queue and returns immediately, which
helps avoid thread deadlock situations.
This extension method checks the TextBox object to see whether marshaling is required. If
marshaling is required (i.e., when the calling thread is not the one used to create the TextBox
object), the BeginInvoke method is executed. If marshaling is not required, the AppendText
method is called directly on the TextBox object. The BeginInvoke method takes Delegate as
a parameter, so txt.AppendText is cast to an action of String, a general-purpose delegate
that exists in the framework, which represents a call to a method that takes a string parameter.
Now that there is a thread-safe way to display information into the TextBox class, the
AsParallel
example can be performed without risking threading-related exceptions.
AsParallel Extension Method
The AsParallel extension method divides work onto each processor or processor core. The
following code sample starts a stopwatch in the System.Diagnostics namespace to show you
the elapsed time when completed, and then the Enumerable class produces a sequence of
integers, from 1 to 10. The AsParallel method call is added to the source. This causes the
iterations to be spread across the available processor and processor cores. Then a LINQ
query retrieves all the even numbers, but in the LINQ query, the where clause is calling a
Compute method, which has a one-second delay using the Thread class, which is in the
System.
Threading
namespace. Finally, a foreach loop displays the results.
Sample of Visual Basic Code
Private Sub AsParallelToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles AsParallelToolStripMenuItem.Click
Dim sw As New Stopwatch
sw.Start()

Dim source = Enumerable.Range(1, 10).AsParallel()
Dim evenNums = From num In source
Where Compute(num) Mod 2 = 0
Select num
For Each ev In evenNums
txtLog.WriteLine("{0} on Thread {1}", _
New Object() {ev, Thread.CurrentThread.GetHashCode})
Next
sw.Stop()
txtLog.WriteLine("Done {0}", New Object() {sw.Elapsed})
End Sub
Public Function Compute(ByVal num As Integer) As Integer
txtLog.WriteLine("Computing {0} on Thread {1}", _
New Object() {num, Thread.CurrentThread.GetHashCode})
Thread.Sleep(1000)
Return num
End Function
Sample of C# Code
private void asParallelToolStripMenuItem_Click(
object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
var source = Enumerable.Range(1, 10).AsParallel();
var evenNums = from num in source
where Compute(num) % 2 == 0
select num;
foreach (var ev in evenNums)
{
txtLog.WriteLine("{0} on Thread {1}", ev,
Thread.CurrentThread.GetHashCode());
}
sw.Stop();
txtLog.WriteLine("Done {0}", sw.Elapsed);
}
public int Compute(int num)
{
txtLog.WriteLine("Computing {0} on Thread {1}", num,
Thread.CurrentThread.GetHashCode());
Thread.Sleep(1000);
return num;
}
AsEnumerable results, showing even numbers, total time, and computing method:
6 on Thread 10
2 on Thread 10
4 on Thread 10
8 on Thread 10
10 on Thread 10
Done 00:00:05.0393262
Computing 1 on Thread 12
226 CHAPTER 3 Introducing LINQ
Computing 2 on Thread 11
Computing 3 on Thread 12
Computing 4 on Thread 11
Computing 5 on Thread 11
Computing 6 on Thread 12
Computing 7 on Thread 12
Computing 8 on Thread 11
Computing 9 on Thread 12
Computing 10 on Thread 11
The output from the Compute calls always shows after the foreach (Visual Basic For Each)
loop output because BeginInvoke marshalls calls to the UI thread for execution when the
UI thread is available. The foreach loop is running on the UI thread, so the thread is busy
until the loop completes. The results are not ordered. Your result will vary as well, and, in
some cases, the results might be ordered. In the example, you can see that the foreach loop
displayed the even numbers, using the main thread of the application, which was thread 10
on this computer. The Compute method was executed on a different thread, but the thread
is either 11 or 12 because this is a two-core processor. Although the Compute method has a
one-second delay, it took five seconds to execute because only two threads were allocated,
one for each core.
In an effort to get a clearer picture of PLINQ, the writing to a TextBox has been replaced
in the following code. Instead of using TextBox, Debug.WriteLine is used, which removes the
requirement to marshall calls back to the UI thread.
Sample of Visual Basic Code
Private Sub AsParallel2ToolStripMenuItem_Click( _
ByVal sender As System.Object, ByVal e As System.EventArgs) _
Handles AsParallel2ToolStripMenuItem.Click
Dim sw As New Stopwatch
sw.Start()
Dim source = Enumerable.Range(1, 10).AsParallel()
Dim evenNums = From num In source
Where Compute2(num) Mod 2 = 0
Select num
For Each ev In evenNums
Debug.WriteLine(String.Format("{0} on Thread {1}", _
New Object() {ev, Thread.CurrentThread.GetHashCode}))
Next
sw.Stop()
Debug.WriteLine(String.Format("Done {0}", New Object() {sw.Elapsed}))
End Sub
Sample of C# Code
private void asParallel2ToolStripMenuItem_Click(
object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
var source = Enumerable.Range(1, 10).AsParallel();
var evenNums = from num in source

where Compute2(num) % 2 == 0
select num;
foreach (var ev in evenNums)
{
Debug.WriteLine(string.Format("{0} on Thread {1}", ev,
Thread.CurrentThread.GetHashCode()));
}
sw.Stop();
Debug.WriteLine(string.Format("Done {0}", sw.Elapsed));
}
public int Compute2(int num)
{
Debug.WriteLine(string.Format("Computing {0} on Thread {1}", num,
Thread.CurrentThread.GetHashCode()));
Thread.Sleep(1000);
return num;
}
The result:
Computing 2 on Thread 10
Computing 1 on Thread 6
Computing 3 on Thread 10
Computing 4 on Thread 6
Computing 5 on Thread 10
Computing 6 on Thread 6
Computing 7 on Thread 10
Computing 8 on Thread 6
Computing 9 on Thread 10
Computing 10 on Thread 6
2 on Thread 9
4 on Thread 9
6 on Thread 9
8 on Thread 9
10 on Thread 9
Done 00:00:05.0632071
The result, which is in the Visual Studio .NET Output window, shows that there is no waiting
for the UI thread. Once again, your result will vary based on your hardware configuration.
ForAll Extension Method
When the query is iterated by using a foreach (Visual Basic For Each) loop, each iteration is
synchronized in the same thread, to be treated one after the other in the order of the sequence.
If you just want to perform each iteration in parallel, without any specific order, use
the ForAll method. It has the same effect as performing each iteration in a different thread.
Analyze this technique to verify that you get the performance gain you expect. The following
example shows the use of the ForAll method instead of the For Each (C# foreach) loop.
Sample of Visual Basic Code
Private Sub ForAllToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
228 CHAPTER 3 Introducing LINQ
Handles ForAllToolStripMenuItem.Click
Dim sw As New Stopwatch
sw.Start()
Dim source = Enumerable.Range(1, 10).AsParallel()
Dim evenNums = From num In source
Where Compute2(num) Mod 2 = 0
Select num
evenNums.ForAll(Sub(ev) Debug.WriteLine(string.Format(
"{0} on Thread {1}", ev, _
Thread.CurrentThread.GetHashCode())))
sw.Stop()
Debug.WriteLine((string.Format("Done {0}", New Object() {sw.Elapsed}))
End Sub
Sample of C# Code
private void forAllToolStripMenuItem_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
var source = Enumerable.Range(1, 10).AsParallel();
var evenNums = from num in source
where Compute(num) % 2 == 0
select num;
evenNums.ForAll(ev => Debug.WriteLine(string.Format(
"{0} on Thread {1}", ev,
Thread.CurrentThread.GetHashCode())));
sw.Stop();
Debug.WriteLine(string.Format("Done {0}", sw.Elapsed));
}
ForAll result, showing even numbers, total time, and computing method:
Computing 1 on Thread 9
Computing 2 on Thread 10
Computing 3 on Thread 9
2 on Thread 10
Computing 4 on Thread 10
Computing 5 on Thread 9
4 on Thread 10
Computing 6 on Thread 10
Computing 7 on Thread 9
6 on Thread 10
Computing 8 on Thread 10
Computing 9 on Thread 9
8 on Thread 10
Computing 10 on Thread 10
10 on Thread 10
Done 00:00:05.0556551
Like the previous example, the results are not guaranteed to be ordered, and there is no
attempt to put the results in a particular order. This technique can give you better performance
as long as this behavior is acceptable.

AsOrdered Extension Method
Sometimes, you must maintain the order in your query, but you still want parallel execution.
Although this will come at a cost, it’s doable by using the AsOrdered extension method. The
following example shows how you can add this method call right after the AsParallel method
to maintain order.
Sample of Visual Basic Code
Private Sub AsOrderedToolStripMenuItem_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles AsOrderedToolStripMenuItem.Click
Dim sw As New Stopwatch
sw.Start()
Dim source = Enumerable.Range(1, 10).AsParallel().AsOrdered()
Dim evenNums = From num In source
Where Compute2(num) Mod 2 = 0
Select num
evenNums.ForAll(Sub(ev) Debug.WriteLine(string.Format(
"{0} on Thread {1}", ev, _
Thread.CurrentThread.GetHashCode())))
sw.Stop()
Debug.WriteLine(string.Format("Done {0}", New Object() {sw.Elapsed}))
End Sub
Sample of C# Code
private void asOrderedToolStripMenuItem_Click(object sender, EventArgs e)
{
Stopwatch sw = new Stopwatch();
sw.Start();
var source = Enumerable.Range(1, 10).AsParallel().AsOrdered();
var evenNums = from num in source
where Compute2(num) % 2 == 0
select num;
evenNums.ForAll(ev => Debug.WriteLine(string.Format(
"{0} on Thread {1}", ev,
Thread.CurrentThread.GetHashCode())));
sw.Stop();
Debug.WriteLine(string.Format("Done {0}", sw.Elapsed));
}
AsOrdered result, showing even numbers, total time, and computing method:
Computing 2 on Thread 11
Computing 1 on Thread 10
2 on Thread 11
Computing 4 on Thread 11
Computing 3 on Thread 10
4 on Thread 11
Computing 6 on Thread 11
Computing 5 on Thread 10
6 on Thread 11
Computing 8 on Thread 11
Computing 7 on Thread 10
8 on Thread 11
230 CHAPTER 3 Introducing LINQ
Computing 9 on Thread 11
Computing 10 on Thread 10
10 on Thread 10
Done 00:00:05.2374586
The results are ordered, at least for the even numbers, which is what the AsOrdered extension
method is guaranteeing.

miercuri, 8 februarie 2012

Lambda Expressions

http://blogs.msdn.com/b/ericwhite/archive/2006/10/03/lambda-expressions.aspx

Lambda Expressions

[Blog Map]  [Table of Contents]  [Next Topic]  This blog is inactive.  New blog: EricWhite.com/blog
Note: This article is a topic in a much larger tutorial on writing pure functional code in C#.  Lambda expressions are only one of the pieces in a much larger puzzle.  Functional programming in C# allows us to write shorter programs that perform better, and are much more robust.  Query Composition using Functional Programming Techniques explains lambda expressions in their proper context.
In order to learn functional programming and a more declarative style of writing code, we need first to cover some basic material.  One of the first concepts is that of lambda expressions.  Lambda expressions can be summarized in one sentence:
Lambda expressions are simply functions/methods.
They have a different syntax, primarily so that they can be written in expression context (more on this shortly) instead of as a member of a class.  However, that is all they are.  For instance, the following lambda expression:
c => c + 1
is a function that takes one argument, c, and returns the value c + 1.
Actually, they are slightly more complicated than this, but not much more.  For the purposes of this tutorial, you only use lambda expressions when calling a method that takes a delegate as a parameter.  Instead of writing a method, creating a delegate from the method, and passing the delegate to the method as a parameter, you can simply write a lambda expression in-line as a parameter to the method.
To show lambda expressions in context, consider the problem where you have an array with 10 digits in it, and you want to filter for all digits greater than 5.  In this case, you can use the Where extension method, passing a lambda expression as an argument to the Where method:
int[] source = new[] { 3, 8, 4, 6, 1, 7, 9, 2, 4, 8 };

foreach (int i in source.Where(x => x > 5))
    Console.WriteLine(i);
To understand the semantics of this code, you needn't find some method elsewhere in the source code that does the selection; the code that you need to read is much tighter and smaller.  It reflects your intent in a much cleaner fashion.
Later on in this tutorial, you'll see a number of uses of the standard query operators.  Many of the standard query operators, including Where, take delegates as an argument, so this means that we can call them passing a lambda as an argument.
First, a quick review of delegates:

Defining, Creating, and Using a Delegate

In C#, a delegate is a data structure that refers to either a static method, or an object and an instance method of its class.  When you initialize a delegate, you initialize it with either a static method, or a class instance and an instance method.
The following code shows the definition of a delegate and a method that can be used to initialize the delegate:
// Defines a delegate that takes an int and returns an int
public delegate int ChangeInt(int x);

// Define a method to which the delegate can point
static public int DoubleIt(int x)
{
    return x * 2;
}
Now, you can create and initialize an instance of the delegate, and then call it:
ChangeInt myDelegate = new ChangeInt(DelegateSample.DoubleIt);
Console.WriteLine("{0}", myDelegate(5));
This, as you would expect, writes 10 to the console.

Using an Anonymous Method

With C# 2.0, anonymous methods allow you to write a method and initialize a delegate in place:
ChangeInt myDelegate = new ChangeInt(
    delegate(int x)
    {
        return x * 2;
    }
);
Console.WriteLine("{0}", myDelegate(5));

Using a Lambda Expression

With Lambda expressions, the syntax gets even terser:
ChangeInt myDelegate = x => x * 2;
Console.WriteLine("{0}", myDelegate(5));
This lambda expression is an anonymous method that takes one argument x, and returns x * 2.  In this case, the type of x and the type that the lambda returns are inferred from the type of the delegate to which the lambda is assigned.
If you wanted to, you could have specified the type of the argument, as follows:
ChangeInt myDelegate = (int x) => x * 2;
Console.WriteLine("{0}", myDelegate(5));

Using a Lambda with Two Arguments

When using the Standard Query Operators, on occasion, you need to write a lambda expression that takes two arguments.
If you have a delegate that takes two arguments:
// Defines a delegate that takes two ints and returns an int
public delegate int MultiplyInts(int arg, int arg2);
You can declare and initialize a delegate:
MultiplyInts myDelegate = (a, b) => a * b;
Console.WriteLine("{0}", myDelegate(5, 2));

Statement Lambda Expressions

You can write a more complicated lambda expression using statements, enclosing the statements in braces.  If you use this syntax, you must use the return statement, unless the lambda returns void:
int[] source = new[] { 3, 8, 4, 6, 1, 7, 9, 2, 4, 8 };

foreach (int i in source.Where(
        x =>
        {
            if (x <= 3)
                return true;
            else if (x >= 7)
                return true;
            return false;
        }
    ))
    Console.WriteLine(i);
Sometimes developers wonder how to pronounce the => token.
If the lambda expression is a predicate, expressing some condition: c => c.State == "WA" then the => can be spoken as "such that".  In this example, you could say "c such that c dot state equals Washington".  If the lambda expression is a projection, returning a new type: c => new XElement("CustomerID", c.CustomerID); then the => can be spoken as "becomes".  In the above example, you could say "c becomes new XElement with a name of CustomerID and its value is c dot CustomerID".  Or "maps to", or "evaluate to", as suggested in the comments below.  But most often, I just say "arrow".  J
A quick note: predicates are simply boolean expressions that are passed to some method that will use the boolean expression to filter something.  A lambda expression used for projection takes one type, and returns a different type.  More on both of these concepts later.

Lambda Expressions that Return Void

A lambda expression that returns void is not very useful in the context of functional programming because the only possible reason for such a function is that it has side-effects, and is not pure (more on this later in the tutorial), but it is part of C# 3.0 syntax, so I'll cover it here.  Sometimes developers will use a void statement lambda expression for writing an event handler.  This has the benefit that the syntax is terser, and the program is smaller.  In addition, the lambda expression can refer to local variables in the enclosing scope.  This is part of C#'s implementation of closures.  The only way to write a lambda expression that returns void is to write a statement lambda expression.  The following example shows defining a void delegate, declaring an instance of it, and calling it.
// Defines a delegate that takes a string and returns void
public delegate void OutputToConsole(string arg);

static void Main(string[] args)
{
    OutputToConsole o = a => {
        Console.WriteLine(a);
    };
    o("Hello, World");
}
If you write a lambda expression for a delegate that returns void and takes no arguments, it results in interesting syntax:
// Defines a delegate that takes no arguments and returns void
public delegate void OutputHelloToConsole();

static void Main(string[] args)
{
    OutputHelloToConsole o = () =>
    {
        Console.WriteLine("Hello, World");
    };
    o();
}

The Func Delegate Types

The framework defines a number of parameterized delegate types:
public delegate TR Func<TR>();
public delegate TR Func<T0, TR>(T0 a0);
public delegate TR Func<T0, T1, TR>(T0 a0, T1 a1);
public delegate TR Func<T0, T1, T2, TR>(T0 a0, T1 a1, T2 a2);
public delegate TR Func<T0, T1, T2, T3, TR>(T0 a0, T1 a1, T2 a2, T3 a3);
In the above delegate types, notice that if there is only one type parameter, it is the return type of the delegate. If there are two type parameters, the first type parameter is the type of the one and only argument, and the second type is the return type of the delegate, and so on. Many of the standard query operators (which are just methods that you call) take as an argument a delegate of one of these types. These delegate definitions are useful to you when writing your own methods that take a delegate as an argument.
using System;
using System.Collections.Generic;

class Program
{
    static List<T> MyWhereMethod<T>(IEnumerable<T> source,
        Func<T, bool> predicate)
    {
        List<T> l = new List<T>();
        foreach (T item in source)
            if (predicate(item))
                l.Add(item);
        return l;
    }

    static void Main(string[] args)
    {
        int[] source = new[] { 3, 8, 4, 6, 1, 7, 9, 2, 4, 8 };

        List<int> filteredList = MyWhereMethod(source,
            i => i >= 5);
        foreach (int z in filteredList)
            Console.WriteLine(z);
    }
}

The Action Delegate Types

The framework defines a number of parameterized delegate types for delegates that return void:
public delegate void Action();
public delegate void Action<T0>(T0 a0);
public delegate void Action<T0, T1>(T0 a0, T1 a1);
public delegate void Action<T0, T1, T2>(T0 a0, T1 a1, T2 a2);
public delegate void Action<T0, T1, T2, T3>(T0 a0, T1 a1, T2 a2, T3 a3);
Sometimes API designers will include an event that takes one of these delegate types as an argument, and you can write a lambda expression for the argument.  As with the Func delegate types, these delegate definitions are useful to you when writing your own methods that take a delegate as an argument.  This uses the interesting syntax of () => { /* body of void function here */ };
using System;
using System.Collections.Generic;
using System.Threading;

class Program
{
    static void SomeAsynchronousMethod(Action complete)
    {
        // just pretending to be asynchronous in this example
        Thread.Sleep(1000);
        complete();
    }

    static void Main(string[] args)
    {
        SomeAsynchronousMethod(() => { Console.WriteLine("Done"); });
    }
}

Expression Trees

Lambda expressions can also be used as expression trees.  This is an interesting topic, but is not part of this discussion on writing pure functional transformations.