Sander
terug naar het overzicht

One of the programming      guys

Als ontwikkelaar mag ik graag problemen oplossen en nieuwe functionaliteit bouwen. Vanuit mijn creatieve kant hou ik me ook graag bezig met hoe iets bij de eindgebruiker aankomt. Je kunt bij mij dus ook terecht voor de opmaak en layout van een oplossing. Als je mij wilt afleiden, dan lukt dat altijd met een coole nieuwe gadget. Dat is namelijk mijn andere passie.

Excel-like filters for Kendo UI Grid

door Sander 22-3-2012

In my previous post I showed how to set up the grid so that all data operations occur on the server. In this post, I’ll build on that a little further to enable filtering like Excel does.

Here is the default method of filtering:

old

And this is what I want:

new

Excuse the blurs, that is ‘real’ testdata Smile

First we need to set up the little screens that will replace the default filtering. I used a div with some Razor to create a list of checkboxes:

<div id="customers" class="filterDropdown">
        @foreach (var c in ViewBag.CustomerFilterSource)
        {
            var customer = "Customers_" + c;
            <input type="checkbox" id="@customer" name="CustomerName" value="@c" checked="checked" />
            <label for="@customer">@c</label>
            <br />
        }
        <input type="button" id="applyCustomerFilter" value="Apply" />
    </div>

The data comes from a collection in the ViewBag that is loaded when the page is served.

Next thing is getting rid of the default filter window. It took me a while to figure this one out but it turned out to be really simple. I used jQuery to locate all the little filter buttons in the header and remove their click events and adding my own:

$("#reportGrid").find(".k-grid-filter").unbind("click").click(function () {
            var parent = $(this).parent();
            var filterList;
            switch (parent.attr("data-field")) {
                case "CustomerName":
                    filterList = $("#customers");
                    break;
                case "CertificateNumber":
                    filterList = $("#certificates");
                    break;
                case "ReportDate":
                    filterList = $("#reportdate");
                    break;
                case "ReportType":
                    filterList = $("#reporttypes");
                    break;
            }
 
            var offset = $(this).offset();
            filterList.css('top', offset.top + $(this).height()).css('left', offset.left + $(this).width()).slideDown();
            return false;
        });

The filter buttons are easy to locate as they have a specific CSS class applied to them. In the switch-case I determine which popup I want to show when the button is clicked. In the last three lines I place the popup in the same spot as the filter button so that it rolls out from where the user clicked.

Some code for the Apply buttons: (just showing one here)

$("#reportdate").hide();
        $("#applyReportDateFilter").click(function () {
            setDsFilter(generateDsFilter());
            $("#reportdate").slideUp();
        })

The popup is hidden since we only want to see it when one of the filterbuttons is clicked. The click event takes care of resetting the datasource filters and hiding the popup.

Now we get to the fun part! To keep the existing filter functionality of the grid’s datasource intact, we need to generate the filters ourselves. That way, the controller code from my previous post can be left unchanged to pick up the filters that the grid posts.

The filters on the datasource are contained in a single object, this object has a property called ‘logic’ which specifies if this is an AND or an OR query. The other is an array of objects named ‘filters’ that hold the columns and their filter values. So here is what needs to happen: Gather all the checkboxes that are checked, translate them into a valid JSON filter object and apply the new filter to the datasource.

function generateDsFilter() {
 
        var checkboxContainer = $("#checkboxFilters");
        var flt = { logic: "and", filters: [] };
 
        checkboxContainer.find("input:checked").each(function () {
            var name = $(this).attr("name");
            var val = $(this).attr("value");
 
            flt.filters.push({ field: name, operator: "eq", value: val });
        });
 
        return flt;
    }

The checkbox container you see holds all the windows that contain checkboxes. We need all of them so I put the windows together in a div for easy selecting with jQuery.

‘flt’ is the object that we need to fill with the filters. The logic is set to AND because we want an exclusive filter. We only want to see stuff that is checked. In a loop we pass all the checked checkboxes using jQuery and add them to the filters property of the main filter object.

To apply the filters to the datasource I added this function:

function setDsFilter(customFilter) {
        var grid = $("#reportGrid").data("kendoGrid").dataSource.filter(customFilter);
    }

I put this in a separate function because there is one thing we still need to fix. The datasource has no initial filter. With the filter being empty, no data is loaded.

To make sure the grid has an initial filter we need to set the ‘filter’ property on the datasource that is part of the grid code:

dataSource: {
                type: "json",
                serverPaging: true,
                serverSorting: true,
                serverFiltering: true,
                allowUnsort: true,
                filter: generateDsFilter(),
                pageSize: 25,
                transport: {
                    read: {
                        url: "Export/PagedData",
                        type: "POST",
                        dataType: "json",
                        contentType: "application/json; charset=utf-8"
                    },
                    parameterMap: function (options) {
                        return JSON.stringify(options);
                    }
                },
                schema: { data: "Items", total: "TotalItemCount" }
            }

This ensures that when the grid is first created, all the filters are applied BEFORE the data is loaded, in this case, all the data is loaded because all the checkboxes are checked.

When clicking the Apply button in one of the filter windows (see the code somewhere above) we regenerate the filters and apply them to the datasource. The grid will then call the Controller Action and post the filters to the server for you to handle.

Tags:

ASP.NET MVC | Development | Kendo UI

Kendo UI Grid with server paging filtering and sorting (with MVC3)

door Sander 21-3-2012

I gave the Kendo UI framework a try today. I needed a grid that could do anything (filtering, sorting and paging – the real kind, on the server that is).

Going about my coding business I came across some things that I found hard to solve using the existing documentation, which inspired this blogpost.

As I mentioned, this is about the Kendo UI KendoGrid. The definition of the grid itself is very easy to when you use the “getting started” tutorial and I ended up with something like this:

$(document).ready(function () {
        $("#reportGrid").kendoGrid({
            dataSource: {
                type: "json",
                serverPaging: true,
                serverSorting: true,
                serverFiltering: true,
                allowUnsort: true,
                pageSize: 10,
                transport: { 
                    read: { 
                        url: "Export/PagedData",
                        type: "POST",
                        dataType: "json",
                        contentType: "application/json; charset=utf-8"
                    },
                    parameterMap: function(options) {
                        return JSON.stringify(options);
                    }
                },
                schema: { data: "Items", total: "TotalItemCount" }
            },
            pageable: true,
            sortable: true,
            filterable: true,
            columns: [
                    { field: "CustomerName", title: "Klant" },
                    { field: "SerialNumber", title: "Serienummer" },
                    { field: "CertificateNumber", title: "Polisnummer" },
                    { field: "ReportDate", title: "Melddatum" },
                    { field: "ReportType", title: "Meldingtype" },
                    { field: "Description", title: "Omschrijving" }
                ],
        });
    });

One point of interest in this code here, the parameterMap in the transport section of the datasource. Without that mapping there, MVC3 will have a hard time reading the parameters that are posted by the grid. The stringify function translates it to something we can use in our controller action.

Lets take a look at that:

public JsonResult PagedData(int skip, int take, int page, int pageSize, List<SortDescription> sort, FilterContainer filter)
        {
            int itemCount = 0;
            var data = _export.GetData(page - 1, pageSize, out itemCount, filter, sort);
 
            return Json(new ExportModel(data, itemCount), JsonRequestBehavior.AllowGet);
        }

The first four parameters are straightforward and let me handle the paging in my query. Note that you need to define the serverXXXXX properties on the grid for these to be posted to the controller on the server! The schema tells the grid the properties to look for when data is read from the server. When paging serverside, you will also need to tell the grid how many items exist on the server so it can calculate the number of pages to show.

After that, things get interesting. Before I came to the solution I now have I spent a fair amount of time searching high and low on how to solve the parsing of array in the querystring. I gave up on that after finding out about the stringify function for the parameterMap in the grid.

If you want some more insight on how to configuring the datasource, look here:

http://www.kendoui.com/documentation/framework/datasource/configuration.aspx

It has everything on configuring a datasource for the Kendo Grid. I was looking for filtering and sorting, which comes down to this:

When filtering, the grid produces and object that contains an array of objects that hold our filter parameters. It holds the columnname, the filter value and the operator used in the filter.

When sorting, an array of objects is produced where each element has an object that contains the column and sort direction for every sort operation.

Translating that into code, we come to this:

public class SortDescription
    {
        public string field { get; set; }
        public string dir { get; set; }
    }
 
public class FilterContainer
    {
        public List<FilterDescription> filters { get; set; }
        public string logic { get; set; }
    }
 
public class FilterDescription
    {
        public string @operator { get; set; }
        public string field { get; set; }
        public string value { get; set; }
    }
If you look at the parameters in the controlleraction, we have a list of sortdescriptions, and an object that contains a list of filters.

The controller is now aware of the filtering and sorting on the grid and all you have left to do is translate the filters and sorting into a query.

Tags:

ASP.NET MVC | Development

ASP.NET MVC3 Localization

door Sander 23-2-2012

We recently started updating Provisior to support multiple languages using .resx files. All is well and fine with that but we also have a few MVC3 pages that needed localization. For our webforms stuff we depended on the InitializeCulture override to set the correct culture on the threads and that works great. We also have a code expression to do the translations directly in the aspx markup.

The text is fetched from the resource files using a ResourceManager object which is wrapped in a custom class that enables us to get different languages on the fly.

My goals was to re-use this class for MVC3 as well to keep all the language stuff separated from the other code. First we need to set the correct culture on the thread for MVC3 which is relatively simple using a class that is based on the default Controller class in MVC:

public class LocalizedController : Controller
    {
        protected override void ExecuteCore()
        {
            var emp = Session[SessionKeys.CurrentEmployee] as EmployeeDTO;
 
            if (emp != null && !string.IsNullOrEmpty(emp.PreferredLanguage))
            {
                var newCult = new CultureInfo(emp.PreferredLanguage);
 
                Thread.CurrentThread.CurrentCulture =
                Thread.CurrentThread.CurrentUICulture = newCult;
            }
 
            base.ExecuteCore();
        }
    }

The ExecuteCore method is called when MVC starts handling a new request.

Now all you have to do is make all your controllers inherit this new controller. That takes care of any automated culture stuff (We embed MVC pages in a masterpage, which ignores any previously set cultures outside of MVC)

Up next, translating all the DisplayName attributes in the models we use in the MVC pages. Again, we create a custom class. This one is based on the normal DisplayName attribute:

public class LocalizedDisplayNameAttribute : DisplayNameAttribute
    {
        private readonly string _key;
 
        public LocalizedDisplayNameAttribute(string resoureceKey) : base()
        {
            this._key = resoureceKey;
        }
 
        public override string DisplayName
        {
            get
            {
                return ResourceHandler.GetResourceString(this._key);
            }
        } 
    }

This class is then used like this in any given model just like the normal DisplayName atribute:

[LocalizedDisplayName("DNS name")]
public string DnsName { get; set; }

the getter will ask our ResourceHandler for a translation of the text that was supplied as a displayname. Another problem solved.

The last part of the problem is that we used text in our .js files that support the razor views. In our case we only had four occurrences so I decided to use an MVC controller to help me out.

I added a blank controller to the controllers folder of our MVC Area and gave it a single HttpPost action:

public class TranslationController : Controller
    {
        [HttpPost]
        public string GetTranslation(string resourceKey, string culture)
        {
            return ResourceHandler.GetResourceString(resourceKey, culture);
        }
 
    }

This allows us to let the server worry about the translations for us. Using the following javascript we can now post keys to the server and receive their corresponding translations.

function getTranslatedString(key) {
 
    var culture = document.getElementById('uxClientSideCulture').value;
 
    var ret;
 
    $.ajax({
        async: false,
        type: "POST",
        url: "Translation/GetTranslation",
        dataType: 'text',
        data: { resourceKey: key, culture: culture },
        success: function (result) {
            ret = result;
        },
        error: function (err) {
            ret = key;
        }
    });
 
    return ret;
}

With a little help of jQuery we can ask the server for translations of texts anywhere in our scripts (or page for that matter). the uxClientSideCulture is a hidden field that contains the preferred culture of the current user.

Be aware that I took this approach because I had only four text messages in my scripts. Using this for large amounts of translating will probably degrade performance because every call is a new request!

Happy coding! Smile

Tags:

.NET and ISO 3166-1 Country codes.

door Sander 19-1-2012

The ‘co’ (country) attribute in Active Directory is a strange property… You cannot just set it to some value and be done with it. This attribute has two partners in crime and they are picky about the stuff you throw at them!

The attributes I’m talking about are:

  •  
    • co
    • c
    • countryCode

These attributes must be set together at the same time with specific values for them to be stored in Active Directory.

Let’s look at the values you are allowed to use for these properties.. yep, you will need the ISO 3166-1. My first guess was using the CultureInfo and RegionInfo objects that are available in the .NET Framework, but beware! The list of cultures in .NET is incomplete and you cannot get the numeric code from the RegionInfo either.

So we need to set the name of a country, its two-letter name, and some cryptic code all at once. ‘co’ needs the full name of a country, ‘c’ needs a two-letter countrycode and the ‘countryCode’ requires the numeric code. Below you will find some code that has all that stuff ready-made so you don’t have to! All you need to do now is have your code find a country and update a DirectoryEntry.

Here is the code that you can adjust to your needs for the taking! The code has all the countries that are on the ISO 3166-1 list. Feel free to leave a comment if you have an improvement on this code!

Use it any way you want, happy copy-pasting Smile

public class CountryData
    {
        public string Name { get; private set; }
        public string TwoLetterCode { get; private set; }
        public int NumericCode { get; private set; }

        public CountryData(string fullName, string twoLetterCode, int numericCode)
        {
            Name = fullName;
            TwoLetterCode = twoLetterCode;
            NumericCode = numericCode;
        }
    }

    public class CountryInfo
    {
        public readonly ICollection<CountryData> Countries;

        public CountryInfo()
        {
            Countries = new List<CountryData>
            {
                { new CountryData("Afghanistan", "AF", 4) },
                { new CountryData("Åland Islands", "AX", 248) },
                { new CountryData("Albania", "AL", 8) },
                { new CountryData("Algeria", "DZ", 12) },
                { new CountryData("American Samoa", "AS", 16) },
                { new CountryData("Andorra", "AD", 20) },
                { new CountryData("Angola", "AO", 24) },
                { new CountryData("Anguilla", "AI", 660) },
                { new CountryData("Antarctica", "AQ", 10) },
                { new CountryData("Antigua and Barbuda", "AG", 28) },
                { new CountryData("Argentina", "AR", 32) },
                { new CountryData("Armenia", "AM", 51) },
                { new CountryData("Aruba", "AW", 533) },
                { new CountryData("Australia", "AU", 36) },
                { new CountryData("Austria", "AT", 40) },
                { new CountryData("Azerbaijan", "AZ", 31) },
                { new CountryData("Bahamas", "BS", 44) },
                { new CountryData("Bahrain", "BH", 48) },
                { new CountryData("Bangladesh", "BD", 50) },
                { new CountryData("Barbados", "BB", 52) },
                { new CountryData("Belarus", "BY", 112) },
                { new CountryData("Belgium", "BE", 56) },
                { new CountryData("Belize", "BZ", 84) },
                { new CountryData("Benin", "BJ", 204) },
                { new CountryData("Bermuda", "BM", 60) },
                { new CountryData("Bhutan", "BT", 64) },
                { new CountryData("Bolivia, Plurinational State of", "BO", 68) },
                { new CountryData("Bonaire, Sint Eustatius and Saba", "BQ", 535) },
                { new CountryData("Bosnia and Herzegovina", "BA", 70) },
                { new CountryData("Botswana", "BW", 72) },
                { new CountryData("Bouvet Island", "BV", 74) },
                { new CountryData("Brazil", "BR", 76) },
                { new CountryData("British Indian Ocean Territory", "IO", 86) },
                { new CountryData("Brunei Darussalam", "BN", 96) },
                { new CountryData("Bulgaria", "BG", 100) },
                { new CountryData("Burkina Faso", "BF", 854) },
                { new CountryData("Burundi", "BI", 108) },
                { new CountryData("Cambodia", "KH", 116) },
                { new CountryData("Cameroon", "CM", 120) },
                { new CountryData("Canada", "CA", 124) },
                { new CountryData("Cape Verde", "CV", 132) },
                { new CountryData("Cayman Islands", "KY", 136) },
                { new CountryData("Central African Republic", "CF", 140) },
                { new CountryData("Chad", "TD", 148) },
                { new CountryData("Chile", "CL", 152) },
                { new CountryData("China", "CN", 156) },
                { new CountryData("Christmas Island", "CX", 162) },
                { new CountryData("Cocos (Keeling) Islands", "CC", 166) },
                { new CountryData("Colombia", "CO", 170) },
                { new CountryData("Comoros", "KM", 174) },
                { new CountryData("Congo", "CG", 178) },
                { new CountryData("Congo, the Democratic Republic of the", "CD", 180) },
                { new CountryData("Cook Islands", "CK", 184) },
                { new CountryData("Costa Rica", "CR", 188) },
                { new CountryData("C“te d'Ivoire", "CI", 384) },
                { new CountryData("Croatia", "HR", 191) },
                { new CountryData("Cuba", "CU", 192) },
                { new CountryData("Cura‡ao", "CW", 531) },
                { new CountryData("Cyprus", "CY", 196) },
                { new CountryData("Czech Republic", "CZ", 203) },
                { new CountryData("Denmark", "DK", 208) },
                { new CountryData("Djibouti", "DJ", 262) },
                { new CountryData("Dominica", "DM", 212) },
                { new CountryData("Dominican Republic", "DO", 214) },
                { new CountryData("Ecuador", "EC", 218) },
                { new CountryData("Egypt", "EG", 818) },
                { new CountryData("El Salvador", "SV", 222) },
                { new CountryData("Equatorial Guinea", "GQ", 226) },
                { new CountryData("Eritrea", "ER", 232) },
                { new CountryData("Estonia", "EE", 233) },
                { new CountryData("Ethiopia", "ET", 231) },
                { new CountryData("Falkland Islands (Malvinas)", "FK", 238) },
                { new CountryData("Faroe Islands", "FO", 234) },
                { new CountryData("Fiji", "FJ", 242) },
                { new CountryData("Finland", "FI", 246) },
                { new CountryData("France", "FR", 250) },
                { new CountryData("French Guiana", "GF", 254) },
                { new CountryData("French Polynesia", "PF", 258) },
                { new CountryData("French Southern Territories", "TF", 260) },
                { new CountryData("Gabon", "GA", 266) },
                { new CountryData("Gambia", "GM", 270) },
                { new CountryData("Georgia", "GE", 268) },
                { new CountryData("Germany", "DE", 276) },
                { new CountryData("Ghana", "GH", 288) },
                { new CountryData("Gibraltar", "GI", 292) },
                { new CountryData("Greece", "GR", 300) },
                { new CountryData("Greenland", "GL", 304) },
                { new CountryData("Grenada", "GD", 308) },
                { new CountryData("Guadeloupe", "GP", 312) },
                { new CountryData("Guam", "GU", 316) },
                { new CountryData("Guatemala", "GT", 320) },
                { new CountryData("Guernsey", "GG", 831) },
                { new CountryData("Guinea", "GN", 324) },
                { new CountryData("Guinea-Bissau", "GW", 624) },
                { new CountryData("Guyana", "GY", 328) },
                { new CountryData("Haiti", "HT", 332) },
                { new CountryData("Heard Island and McDonald Islands", "HM", 334) },
                { new CountryData("Holy See (Vatican City State)", "VA", 336) },
                { new CountryData("Honduras", "HN", 340) },
                { new CountryData("Hong Kong", "HK", 344) },
                { new CountryData("Hungary", "HU", 348) },
                { new CountryData("Iceland", "IS", 352) },
                { new CountryData("India", "IN", 356) },
                { new CountryData("Indonesia", "ID", 360) },
                { new CountryData("Iran, Islamic Republic of", "IR", 364) },
                { new CountryData("Iraq", "IQ", 368) },
                { new CountryData("Ireland", "IE", 372) },
                { new CountryData("Isle of Man", "IM", 833) },
                { new CountryData("Israel", "IL", 376) },
                { new CountryData("Italy", "IT", 380) },
                { new CountryData("Jamaica", "JM", 388) },
                { new CountryData("Japan", "JP", 392) },
                { new CountryData("Jersey", "JE", 832) },
                { new CountryData("Jordan", "JO", 400) },
                { new CountryData("Kazakhstan", "KZ", 398) },
                { new CountryData("Kenya", "KE", 404) },
                { new CountryData("Kiribati", "KI", 296) },
                { new CountryData("Korea, Democratic People's Republic of", "KP", 408) },
                { new CountryData("Korea, Republic of", "KR", 410) },
                { new CountryData("Kuwait", "KW", 414) },
                { new CountryData("Kyrgyzstan", "KG", 417) },
                { new CountryData("Lao People's Democratic Republic", "LA", 418) },
                { new CountryData("Latvia", "LV", 428) },
                { new CountryData("Lebanon", "LB", 422) },
                { new CountryData("Lesotho", "LS", 426) },
                { new CountryData("Liberia", "LR", 430) },
                { new CountryData("Libya", "LY", 434) },
                { new CountryData("Liechtenstein", "LI", 438) },
                { new CountryData("Lithuania", "LT", 440) },
                { new CountryData("Luxembourg", "LU", 442) },
                { new CountryData("Macao", "MO", 446) },
                { new CountryData("Macedonia, the former Yugoslav Republic of", "MK", 807) },
                { new CountryData("Madagascar", "MG", 450) },
                { new CountryData("Malawi", "MW", 454) },
                { new CountryData("Malaysia", "MY", 458) },
                { new CountryData("Maldives", "MV", 462) },
                { new CountryData("Mali", "ML", 466) },
                { new CountryData("Malta", "MT", 470) },
                { new CountryData("Marshall Islands", "MH", 584) },
                { new CountryData("Martinique", "MQ", 474) },
                { new CountryData("Mauritania", "MR", 478) },
                { new CountryData("Mauritius", "MU", 480) },
                { new CountryData("Mayotte", "YT", 175) },
                { new CountryData("Mexico", "MX", 484) },
                { new CountryData("Micronesia, Federated States of", "FM", 583) },
                { new CountryData("Moldova, Republic of", "MD", 498) },
                { new CountryData("Monaco", "MC", 492) },
                { new CountryData("Mongolia", "MN", 496) },
                { new CountryData("Montenegro", "ME", 499) },
                { new CountryData("Montserrat", "MS", 500) },
                { new CountryData("Morocco", "MA", 504) },
                { new CountryData("Mozambique", "MZ", 508) },
                { new CountryData("Myanmar", "MM", 104) },
                { new CountryData("Namibia", "NA", 516) },
                { new CountryData("Nauru", "NR", 520) },
                { new CountryData("Nepal", "NP", 524) },
                { new CountryData("Netherlands", "NL", 528) },
                { new CountryData("New Caledonia", "NC", 540) },
                { new CountryData("New Zealand", "NZ", 554) },
                { new CountryData("Nicaragua", "NI", 558) },
                { new CountryData("Niger", "NE", 562) },
                { new CountryData("Nigeria", "NG", 566) },
                { new CountryData("Niue", "NU", 570) },
                { new CountryData("Norfolk Island", "NF", 574) },
                { new CountryData("Northern Mariana Islands", "MP", 580) },
                { new CountryData("Norway", "NO", 578) },
                { new CountryData("Oman", "OM", 512) },
                { new CountryData("Pakistan", "PK", 586) },
                { new CountryData("Palau", "PW", 585) },
                { new CountryData("Palestinian Territory, Occupied", "PS", 275) },
                { new CountryData("Panama", "PA", 591) },
                { new CountryData("Papua New Guinea", "PG", 598) },
                { new CountryData("Paraguay", "PY", 600) },
                { new CountryData("Peru", "PE", 604) },
                { new CountryData("Philippines", "PH", 608) },
                { new CountryData("Pitcairn", "PN", 612) },
                { new CountryData("Poland", "PL", 616) },
                { new CountryData("Portugal", "PT", 620) },
                { new CountryData("Puerto Rico", "PR", 630) },
                { new CountryData("Qatar", "QA", 634) },
                { new CountryData("R‚union", "RE", 638) },
                { new CountryData("Romania", "RO", 642) },
                { new CountryData("Russian Federation", "RU", 643) },
                { new CountryData("Rwanda", "RW", 646) },
                { new CountryData("Saint Barth‚lemy", "BL", 652) },
                { new CountryData("Saint Helena, Ascension and Tristan da Cunha", "SH", 654) },
                { new CountryData("Saint Kitts and Nevis", "KN", 659) },
                { new CountryData("Saint Lucia", "LC", 662) },
                { new CountryData("Saint Martin (French part)", "MF", 663) },
                { new CountryData("Saint Pierre and Miquelon", "PM", 666) },
                { new CountryData("Saint Vincent and the Grenadines", "VC", 670) },
                { new CountryData("Samoa", "WS", 882) },
                { new CountryData("San Marino", "SM", 674) },
                { new CountryData("Sao Tome and Principe", "ST", 678) },
                { new CountryData("Saudi Arabia", "SA", 682) },
                { new CountryData("Senegal", "SN", 686) },
                { new CountryData("Serbia", "RS", 688) },
                { new CountryData("Seychelles", "SC", 690) },
                { new CountryData("Sierra Leone", "SL", 694) },
                { new CountryData("Singapore", "SG", 702) },
                { new CountryData("Sint Maarten (Dutch part)", "SX", 534) },
                { new CountryData("Slovakia", "SK", 703) },
                { new CountryData("Slovenia", "SI", 705) },
                { new CountryData("Solomon Islands", "SB", 90) },
                { new CountryData("Somalia", "SO", 706) },
                { new CountryData("South Africa", "ZA", 710) },
                { new CountryData("South Georgia and the South Sandwich Islands", "GS", 239) },
                { new CountryData("South Sudan", "SS", 728) },
                { new CountryData("Spain", "ES", 724) },
                { new CountryData("Sri Lanka", "LK", 144) },
                { new CountryData("Sudan", "SD", 729) },
                { new CountryData("Suriname", "SR", 740) },
                { new CountryData("Svalbard and Jan Mayen", "SJ", 744) },
                { new CountryData("Swaziland", "SZ", 748) },
                { new CountryData("Sweden", "SE", 752) },
                { new CountryData("Switzerland", "CH", 756) },
                { new CountryData("Syrian Arab Republic", "SY", 760) },
                { new CountryData("Taiwan, Province of China", "TW", 158) },
                { new CountryData("Tajikistan", "TJ", 762) },
                { new CountryData("Tanzania, United Republic of", "TZ", 834) },
                { new CountryData("Thailand", "TH", 764) },
                { new CountryData("Timor-Leste", "TL", 626) },
                { new CountryData("Togo", "TG", 768) },
                { new CountryData("Tokelau", "TK", 772) },
                { new CountryData("Tonga", "TO", 776) },
                { new CountryData("Trinidad and Tobago", "TT", 780) },
                { new CountryData("Tunisia", "TN", 788) },
                { new CountryData("Turkey", "TR", 792) },
                { new CountryData("Turkmenistan", "TM", 795) },
                { new CountryData("Turks and Caicos Islands", "TC", 796) },
                { new CountryData("Tuvalu", "TV", 798) },
                { new CountryData("Uganda", "UG", 800) },
                { new CountryData("Ukraine", "UA", 804) },
                { new CountryData("United Arab Emirates", "AE", 784) },
                { new CountryData("United Kingdom", "GB", 826) },
                { new CountryData("United States", "US", 840) },
                { new CountryData("United States Minor Outlying Islands", "UM", 581) },
                { new CountryData("Uruguay", "UY", 858) },
                { new CountryData("Uzbekistan", "UZ", 860) },
                { new CountryData("Vanuatu", "VU", 548) },
                { new CountryData("Venezuela, Bolivarian Republic of", "VE", 862) },
                { new CountryData("Viet Nam", "VN", 704) },
                { new CountryData("Virgin Islands, British", "VG", 92) },
                { new CountryData("Virgin Islands, U.S.", "VI", 850) },
                { new CountryData("Wallis and Futuna", "WF", 876) },
                { new CountryData("Western Sahara", "EH", 732) },
                { new CountryData("Yemen", "YE", 887) },
                { new CountryData("Zambia", "ZM", 894) },
                { new CountryData("Zimbabwe", "ZW", 716) }
            };
        }

        public CountryData GetCountryByNumericCode(int code)
        {
            var data = Countries.FirstOrDefault(cd => cd.NumericCode == code);

            if (data == null)
                throw new ArgumentException(string.Format("{0} is an invalid numeric countrycode!", code), "code");

            return data;
        }

        public CountryData GetCountryByNameOrCode(string nameOrTwoLetterCode)
        {
            var data = Countries.FirstOrDefault(cd => string.Equals(cd.Name, nameOrTwoLetterCode, StringComparison.InvariantCultureIgnoreCase) || string.Equals(cd.TwoLetterCode, nameOrTwoLetterCode, StringComparison.InvariantCultureIgnoreCase));

            if (data == null)
                throw new ArgumentException(string.Format("CountryData with name or two letter code {0} does not exist!", nameOrTwoLetterCode), "nameOrTwoLetterCode");

            return data;
        }
    }

Tags:

Persist Telerik RadTreeView (ASP.NET Ajax) state in cookie

door Sander 24-11-2011

Finding myself without a nice example of how to accomplish this, there was no other way of doing this myself :-)

First, we need to find out which nodes are expanded. The right time to check for this is when nodes are expanded/collapsed. So here is the client-side function:

function uxProfileTree_NodeToggle(sender, args) {
    var tree = $find("<%= uxProfileTree.ClientID %>");
    var allNodes = tree.get_allNodes();
    var nodeString = "";
    for (var i = 0; i < allNodes.length; i++) {
        var node = allNodes[i];
        var expanded = node.get_expanded();
            if (expanded) {
                nodeString = nodeString + node.get_value() + "*";
                createCookie("preofileTreeState", nodeString, 365);
        }
    }
}

This function is hooked into the OnClientCollapsed and OnClientExpanded client-side handlers of the treeview that you want to persist.

We now know what nodes are expanded so lets put the nodeString to work. In the Page_LoadComplete handler of my ASP.NET page I check for the cookie we created on the client and parse its value into an array by splitting the nodeString at the * char.

HttpCookie cookie = Request.Cookies[treeCookieName];
            try
            {
                if (cookie != null)
                {
                    string[] toggleParts = cookie.Value.Split(new[] { "*" }, StringSplitOptions.RemoveEmptyEntries);
                    foreach (string part in toggleParts)
                    {
                        RadTreeNode toggledNode = uxProfileTree.FindNodeByValue(part);
 
                        toggledNode.Expanded = true;
                    }
                }
            }
            catch
            {
                // Remove the cookie if it messes up!
                Request.Cookies.Remove(treeCookieName);
            }

For every value we find in the array, we look up the corresponding node in the tree. The only thing left is setting the node’s Expanded property to true. As a failsafe, the cookie is removed if anything messes up while trying to find the matching node. This is to make sure the user isn’t stuck with a broken cookie and a tree that wont expand its nodes.

Tags:

Telerik | Development

Renaming an object in Active Directory

door Sander 28-10-2011

This took me well over an hour to figure out and even though I now know how to get around the problem, I still don’t know what’s causing it.

I have a piece of code that calls the DirectoryEntry.Rename(…) method. It does exactly what is says on the box, it renames the entry. (see here).

The first pitfall you’ll encounter is the input the method expects. The MSDN article states that it expects the new name, but that’s not how it works. You need to prefix it with CN= (or OU= in case of an Organizational Unit).

But now for the real issue.

Me method takes a SID (as string) as a parameter which is used to look up the entry like this:

   1:  using (var targetGroup = new DirectoryEntry(string.Format("{0}<SID={1}>", ConfigSettings.LdapPrefix, groupSid)))
   2:  {
   3:  }

Works great and does what needs to be done. But I also want to rename the targetGroup entry.

   1:  using (var targetGroup = new DirectoryEntry(string.Format("{0}<SID={1}>", ConfigSettings.LdapPrefix, groupSid)))
   2:  {
   3:      targetGroup.Rename(string.Format("CN={0}", newGroupName));
   4:  }
But guess what…when you call it like this, it throws a NotImplementedException

As I said, I don’t know what is causing this (bug in the framework?), but there is a way around it:

   1:  using (var targetGroup = new DirectoryEntry(string.Format("{0}<SID={1}>", ConfigSettings.LdapPrefix, groupSid)))
   2:  {
   3:      var groupDn = AdUtil.GetProperty(targetGroup, "distinguishedName");
   4:      using (var actualGroup = new DirectoryEntry(string.Format("{0}{1}", ConfigSettings.LdapPrefix, groupDn))
   5:      {
   6:          actualGroup.Rename(string.Format("CN={0}", newGroupName));
   7:      }
   8:  }

So here’s the trick, bind to the entry via its SID, then bind to it again with a new DirectoryEntry object using the distinguishedName of the previous object. It’s not pretty, but now you can rename your object without being slapped with a NotImplementedException.

Tags:

Setting up a wireless 3G connection in Windows

door Sander 5-10-2011

I found myself without Wi-Fi the other day and in need of internet access. My notebook is equipped with a wireless 3G adapter but the last time I repaved the it I forgot the little tool that comes with the 3G card to configure the connection. Still in need of a connection I assumed there had to be a native Windows way of doing things because I did install the proper drivers for the card. I had some trouble figuring out where to find the right setup wizard to set it up so here it is:

The card is a Huawei somethingsomethingcard (not sure what type it is) and I’m using a Vodafone SIMcard. I did this on the Windows 8 Dev Preview but my guess is it will work the same on a Windows 7 machine Smile

First, we need to create a new connection. I want to go online so the ‘Connect to the internet’ option is the obvious choice. The next choice in the wizard is the Dial-up connection. When you select this option, you'll need to know a few thing about the mobile network you are connecting to. For the Vodafone network these are:

  • The number that needs to be dialed for the connection.
  • A username and password.

The number in this case is *99# and the username and password are both vodafone. Please note that these settings depend on the provider you are using! I did not encounter a screen that required me to put in the APN (live.vodafone.com) but it worked even without this Smile

Put in all the settings and hit connect. It took a while before the connection started working so give it some time before hitting the ‘Skip’ button Smile

A step by step walkthrough:

1

2

3

4

5

I would normally do all these settings though the tool that comes with the notebook (Acer 3G Connection Manager) but this way there is no need for the 3G tool at all. The tool however is prone to hanging… This way: no more crashes!

Tags:

Telerik RadChart Dynamic Height

door Sander 21-7-2011

Something nice again today, one of our clients had an issue with the way a graph was showing its data. The issue is the fixed height of the radchart. At first it did have a fixed height, but that has two problems. One is displayed here on these images:

1bar_before  2bars_before

Now imagine this moving forward to about 40 to 50 bars in the graph… that was the issue reported by our client. The data wouldn’t fit inside the rectangle the graph was rendering.

 

I wanted to solve two things here:

    - The height must be dynamic (depending no how many bars are showing)

    - The bars must all have the same height to keep things uniform.

 

Since all my data is loaded at Page_Load that’s the place I’m going to address this.

I know how many bars will be rendered in advance which is the first number in my calculation. The second number I need is the amount of dead space between the edge of the chart and the area where the bars are drawn.

before_borders

 

The second number of our calculation is found using the IE ruler (Press F12, go to Tools). I measure from the topmost border of the chart to the edge of the area where the bars are drawn. I found that with my chart setup the gap is 65 pixels

 

Now for the fix. Say I have 2 bars that need rendering and I want each bar to be 25 pixels high. In the event where my data is bound to the grid I set the height to:

 

<numberOfBars (2)> * 25 + 65 = 115px

Translating that to code:

uxChart.Height = new Unit((dataItems.Count() * 25) + 65);

(dataItems is my charts datasource.)

In my case, this will give me this:

1bar_after  2bars_after

 

Of even with three bars:

3bars_after

 

The RadChart now scales correctly no matter how many bars I need to display. It will just keep on growing according to the amount of bars.

Tags:

Hidden Validator…

door Sander 20-7-2011

Working on some large complex ASP.NET thing and have a client-side validator blocking a postback but you cant see which one because it is invisible? Check… The validator that was blocking the postback was not visible, but was still enabled. 

 

Page.IsValid will obviously say false when a postback occurs and a validator is tripped. In my case the validator tripped without me knowing about it. You could call that PEBCAK but it had to be solved. Since I didn’t want to spend to much time finding the annoying validator I used trusty old LINQ to find it for me.

 

At the point Page.IsValid is checked, I wrote this line and slapped a breakpoint on it:

var annoyingValidator = Page.Validators.OfType<IValidator>().Where(v => !v.IsValid);

From there I could see the errormessages of the validators that were being tripped, Ctrl + F and hey presto. Added some code to make sure the validator is disabled at the right time and problem solved.

Tags:

Find a computer in Active Directory

door Sander 8-6-2011

I’m pretty sure this is covered in some post already on the world wide interwebs but since it took me some time to figure this one out I’m putting it up here.

 

For those of you who write code that talks to Active Directory (using the DirectorySearcher object in this case), this is a nice little detail.

 

The code I was working on validates a computername supplied by the user. Since I already had code that can find a user via their SAMAccountName, I decided to go down that same route.

 

Here is the code for a DirectorySearcher that finds users by their SAMAccountName:

var searcher = new DirectorySearcher(rootEntry)
{
    SearchScope = SearchScope.Subtree,
    Filter = string.Format("(&(sAMAccountName={0})(objectClass=user))", 
        dHelper.EscapeForLdap(sAMAccountName))
};

So my guess was:

var searcher = new DirectorySearcher(rootEntry)
{
    SearchScope = SearchScope.Subtree,
    Filter = string.Format("(&(sAMAccountName={0})(objectClass=computer))", 
        AdHelper.EscapeForLdap(computerSAMAccountName))
};

The code ran fine, but it never found any computers even though they existed in Active Directory. What’s up with that?

 

After some digging around I came across this tiny little detail using ADSI Edit:

 

adsi

 

So there is a dollar ($) sign appended there? Hmm, creating a new computer also adds the dollar sign to the end of the supplied name. Using this info I continued my search and ended up here:

 

http://msdn.microsoft.com/en-us/library/aa370254(v=vs.85).aspx

 

Aha!

 

A computer account name always has a trailing dollar sign ($). Any functions used to manage computer accounts must build the computer name such that the last character of the computer account name is a dollar sign ($). For interdomain trust, the account name is TrustingDomainName$.

 

Changing the Filter to this:

Filter = string.Format("(&(sAMAccountName={0}$)(objectClass=computer))", 
    AdHelper.EscapeForLdap(computerSAMAccountName))
Solves my filtering problem and the method is now returning the correct results.

Tags: