Files

Please download the example solution for this post from the following url.

http://lab.andrewrea.co.uk/ResourcesExampleMvc.rar

Summary

Ok, I have been writing this one as I went, whilst thinking and deving so my opinion on this has changed from when I started writing this to what I ended up with and I have to say I am quite chuffed with the outcome as it yields some other possibilities which I want to now blog about, but to cut a long story/blog post short I have made a Http Handler which accepts some parameters so it can locate a resource file, enumerate through its properties and output them as JavaScript variables to the Response Output Stream. This allows me to expose resources the application is using to client script so I am not duplicating any of them and making maintenance and additions much easier.

I have also wrapped in a bit of token based security, although I do kind of ensure that what is attempted to be enumerated is a resource file by passing the type into a ResourceManager I have added the token based security regardless.  Another good option would be to encrypt the url like the common .axd resource handler does, but either way I have used a token based approach using HMAC and SHA1.

Example usage:

<%= Html.ClientResourceLink<ResourcesExampleMvc.Core.Resources.Global>("jsloc.axd","fr") %>

Example output:

<script type="text/javascript" src="/jsloc.axd?typeName=ResourcesExampleMvc.Core.Resources.Web.Controllers.Home.Home&culture=fr&token=2E935FA9EF23865A9A57B437EC9A7CCE62A7712B&timestamp=1260744495"></script> 

The handler at work:

var cache_date = "13 December 2009 23:57";
var GlobalString1 = "French Global String 1";

image

Finally I have made a HtmlHelper which will generate the relevant link / links I need.  So the blog that I started writing, dev’d and changed my mind…

here goes…

I have been looking at different examples on the web relating to client side localisation.  The two main points I see are:

  1. Add secondary resources inside JavaScript files (This leads to duplication of resources)
  2. Make a call to the Resource Manager inside delimiters inside the mark-up. (This is fine as long as you are not using separate JavaScript Files)

I might be wrong, but from what I can see on the Resource Manager, there is only a set way of getting access to a resource, and that is by its name, so if I want to get several related items, I need to know the keys of each.  As I am writing this I am now starting to think if what I originally wanted to do is as efficient as I first thought.

Basically I thought that inside each Resource file that is defined, each key could be prefixed with some kind of grouping information, e.g. HomeController , so I could have the following keys:

  • HomeController_WelcomeText
  • HomeController_Button1
  • HomeController_Button2

With this I then thought of making a way to query the resources, so I could apply a prefix and then get in return all matched keys.  I would do this by reflection and looping over each property, storing its name and value.

So to summarise I was stuck in the way of thinking one resource file and differentiating based on the string key.  DOH!, thinking about it now, I think the best and most clean solution is to use a separate resource file per each localisable entity i.e. a Form, or Controller etc…  If you think about it also, this will make things much more organised when you get to a point where you have thousands of resources.  Having them logically separated in line with the entity which will be localised makes good sense.  Not only that it also make the task I was thinking about much easier.

The Idea

If you think about ASP.NET MVC for example, and the following. You will always have resources which are common or global and then you will have resources which are specific to a certain part of the project, e.g. Home Controller.

I want to be able to output any localisation that I need so that I can consume with JavaScript without any unnecessary AJAX calls.  More so I only want to output the required keys from the global resources and the resources specific to the area which it is currently being executed, i.e. Home Controller.

The format which I am thinking for the output of the client side localisation is simply a included JavaScript file with the contents simply declared variables which match the name of those inside the resource files i.e.

var Global_Resource_Key_1 = "Hello World";
var Global_Resource_Key_2 = "Hello Galaxy";
var Local_Resource_Key_1 = "Local Number 1";
var Local_Resource_Key_2 = "Local Number 2";

Going back to what I said above, these will be output by a method using reflection to iterate through each of its properties to then generate the required output.  Once the iteration has complete it would be wise to store the resulting collection in Cache or Application object.  I am thinking that the generation of the script will be using a HttpHandler, allowing for the variables to be dynamic inside the mark-up and script  declaration.

Thinking about it more, this is exactly why we are given the special .NET folders of :

  • App_GlobalResources
  • App_LocalResources

These are great, but, I need to have the resource files inside another assembly so that they can be referenced both from the web and also internally from the calling assembly.  So inside a test project I have done the following to setup:

image

  • Create a MVC Application
    • Delete the Account View and Controllers
    • Move the Controllers, Global.asax.cs and Models to the Class Library project
  • Create a separate Class Library Project
    • Create a folder for resources
    • Create a Global Resources and also a Resource file per Controller with the relevant folder structure
    • Created a Configuration folder and class to handle the secret key for hmac’ing and cache timeout
    • Created an Extension Method folder and HtmlHelper class which will be a shortcut for the deveoper to use which will generate the link
    • Created a HttpHandlers folder and the actual handler which I will use to generate and cache the relevant properties

Also, I have omitted any DI/IOC for the purposes of this example. I will go through each of the Class Library project sections separately.

Create a folder for resources & Create a Global Resources and also a Resource file per Controller with the relevant folder structure

Having a separate folder in the resource means I can consume these from the web application but also from any models which are inside the assembly or business data for example where I may state the resource for validation attributes like those used in the DataAnnotations or the MVAB (Microsoft Validation Application Block).

I have but the global resource file directly inside this folder and then created sub folders to reflect different parts of the application which the one being in this instance, Web and then even further by controller.  I think grouping the resources by Controller for the web is a logical step, and along with a global resource file, you are pretty much covered.

image

When you build the solution, .NET will logically group your assemblies by culture, so have many resource files still means they will compile down into one assembly, which is great.

Created a Configuration folder and class to handle the secret key for hmac’ing and cache timeout

I could quite as easily have used the AppSettings but I thought that it would be good to give this attempt its own configuration section, which I could extend and keep encapsulated in the future.  The two things which I am using this section for at the moment is to store the key I will use to generate the HMAC hashes and also the sliding timeout for the cache of the resources.  The secret key can be anything you want, but I needed it in a centralised place so i can ensure the same one is used to generate and also compare.  There is not much to the Configuration Section accept a couple of required attributes and the syntax for retrieving the value declared in the config file.

<clientResourceConfiguration 
	resourceSlidingTimeout="20" 
	resourceHmacKey="470F0BE4675941baBEFBC1134CC1FEAF28C47C15D71543b8A9F57360CFFCD33B"/>

The secret key / resourceHmacKey here is simply two GUIDs stripped of the curlies and dashes and concatenated together.  To reference this configuration inside the code, I have placed a static property on the MvcApplication class inside the Global.asax.cs file.  Seemed like a logical place to put it, and of course made it a singleton.

    public class MvcApplication : System.Web.HttpApplication
    {
        private static ClientResourceConfiguration _clientResourceConfiguration = null;

        public static ClientResourceConfiguration ClientResourceConfiguration
        {
            get
            {
                if (_clientResourceConfiguration == null)
                {
                    _clientResourceConfiguration = (ClientResourceConfiguration)ConfigurationManager.GetSection("clientResourceConfiguration");
                }
                return _clientResourceConfiguration;
            }
        }

...

Created an Extension Method folder and HtmlHelper class which will be a shortcut for the developer to use which will generate the link

This is simply to make it easier for the developer to create the link.  You can simply add the type you want to parse, the name of the handler which is mapped in the config file and the culture.  In this implementation I have designed it to expect the two letter culture name and then go on to resolve the specific culture.  I know it would have been easier to just specify the specific culture but I dev’d this with a work related problem I had and wanted to simulate the environment in which I have to work with.

I have created an overloaded method so that if I need to I can force clear the cache for a specific handler.  As I will show you further down I also output the date it was cached, again simply for diagnostic purposes.

The token here is simply so i can be sure that the resource file which is being requested has been authorized by the server, as it is the server which is the only entity that has the secret key and can create such links.  I have included a timestamp which makes the HMAC hash different each time.  The validation of this token will only occur if the requested resource is not in the cache, as I make the assumption that is if it is in the cache, then it has to have been generated for a valid reason by the server.

Oh and there is a small method in there I found on Brad Abrams site which simply gives me back the number of seconds since 1970, which acts as a timestamp.

    public static class HtmlHelpers
    {
        public static string ClientResourceLink<T>(this HtmlHelper helper, string handler, string twoLetterCultureName)
        {
            return ClientResourceLink<T>(helper, handler, twoLetterCultureName, true);
        }

        public static string ClientResourceLink<T>(this HtmlHelper helper, string handler, string twoLetterCultureName, bool cache)
        {
            var typeName = typeof(T).FullName;
            var timeStamp = GetTimeStamp().ToString();
            var valueToHash = String.Concat(typeName, twoLetterCultureName, timeStamp);
            var token = CryptoHelper.Hmac(valueToHash, MvcApplication.ClientResourceConfiguration.ResourceHmacKey, HashType.SHA1);
            var url = String.Format("~/{0}?typeName={1}&culture={2}&token={3}&timestamp={4}",
                handler,
                typeName,
                twoLetterCultureName,
                token,
                timeStamp);
            if (!cache)
            {
                url += "&cache=ncache";
            }
            return String.Format("<script type=\"text/javascript\" src=\"{0}\"></script>",
                new UrlHelper(helper.ViewContext.RequestContext).Content(url));
        }

        /// <summary>
        /// From Brad Abrams : http://blogs.msdn.com/brada/archive/2004/03/20/93332.aspx
        /// </summary>
        /// <returns></returns>
        private static int GetTimeStamp()
        {
            TimeSpan t = (DateTime.UtcNow - new DateTime(1970, 1, 1));
            int timestamp = (int)t.TotalSeconds;
            return timestamp;
        }
    }

The CryptoHelper class is one which “I made earlier,” and which I blogged about here http://www.andrewrea.co.uk/2009/10/05/ACryptographyHelperClassForHashingAndForKHMACKeyedHashMessageAuthenticationCode.aspx.  It is simply a helper method wrapping around some types and methods inside the System.Security.Cryptography namespace.  You will see some example usages in the summary above.

Created a HttpHandlers folder and the actual handler which I will use to generate and cache the relevant properties

This is basically the crooks of the solution and it is the handler.  I have used the .axd extension simply because it is already recognised and is ignored my the MVC route handler. 

Below is the entry I have used to configure the Http Handler for GET only and an example path to map it to

<add verb="GET" path="/jsloc.axd" validate="false" type="ResourcesExampleMvc.Core.HttpHandlers.ClientSideResourceHttpHandler,ResourcesExampleMvc.Core" />

It is in this class where I handle:

  • The parameters passed in
  • The validation of the token
  • The cache of the resources for the client
  • The generation of the resources

I simply set the Response.ContentType to text/javascript and then write out the information through a StreamWriter.  The first variable I add is the CacheDate and then followed by the resources themselves, as they appear inside the resource file.  A point to mention here is if you have defined any of the keys in the resource files with spaces in they will be replaced with underscores.

Major Point : You must set the scope of your Resource File, which ever one you want the Handler to parse as Public, this is due to me put Binding Flags on the reflection as Public and Static.  I suppose I could have added Internal, but not sure, so I will leave for now as Public.

    public class ClientSideResourceHttpHandler : IHttpHandler
    {
        #region IHttpHandler Members

        public bool IsReusable
        {
            get { return true; }
        }

        public void ProcessRequest(HttpContext context)
        {
            string typeName = context.Request.QueryString["typeName"];
            string twoLetterCultureName = context.Request.QueryString["culture"];
            string token = context.Request.QueryString["token"];
            string timestamp = context.Request.QueryString["timestamp"];
            string nocache = context.Request.QueryString["nocache"];
            var timeout = MvcApplication.ClientResourceConfiguration.ResourceSlidingTimeout;

            string key = GetKey(typeName, twoLetterCultureName);

            if (context.Cache[key] == null || !String.IsNullOrEmpty(nocache))
            {
                var type = Type.GetType(typeName);
                var resourceManager = new ResourceManager(type);

                if (!ValidateToken(typeName, twoLetterCultureName, timestamp, token))
                    throw new SecurityException("Invalid token submitted for client resource");

                var list = new List<KeyValuePair<string, string>>();
                var properties = type.GetProperties(
                    System.Reflection.BindingFlags.Public |
                    System.Reflection.BindingFlags.Static);

                foreach (var property in properties)
                {
                    if (property.PropertyType == typeof(string))
                    {

                        list.Add(
                            new KeyValuePair<string, string>(property.Name,
                                resourceManager.GetString(property.Name.Replace("_", " "),
                                GetCulture(twoLetterCultureName))
                                )
                        );
                    }
                }

                context.Cache.Insert(key, list, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(timeout));
            }

            context.Response.ContentType = "text/javascript";
            WriteOutClientResources(context.Response.OutputStream, (List<KeyValuePair<string, string>>)context.Cache[key]);
            context.Response.End();
        }

        #endregion

        private void WriteOutClientResources(Stream outputStream, List<KeyValuePair<string, string>> values)
        {
            using (var sw = new StreamWriter(outputStream))
            {
                sw.WriteLine(String.Format("var cache_date = \"{0}\";", DateTime.Now.ToString("f")));
                foreach (var item in values)
                {
                    sw.WriteLine(String.Format("var {0} = \"{1}\";", item.Key, item.Value));
                }
                sw.Flush();
            }
        }

        private bool ValidateToken(string typeName, string twoLetterCultureName, string timestamp, string token)
        {
            var valueToHmac = String.Concat(typeName, twoLetterCultureName, timestamp);
            var valueToCompare = CryptoHelper.Hmac(valueToHmac, MvcApplication.ClientResourceConfiguration.ResourceHmacKey, HashType.SHA1);
            return valueToCompare.Equals(token);
        }

        private string GetKey(string controllerName, string twoLetterCultureName)
        {
            return String.Format("{0}!{1}", controllerName, twoLetterCultureName);
        }

        private CultureInfo GetCulture(string twoLetterCultureName)
        {
            switch (twoLetterCultureName)
            {
                case "fr":
                    return CultureInfo.CreateSpecificCulture("fr-FR");
                default:
                    return CultureInfo.CreateSpecificCulture("en-GB");
            }
        }
    }

If you did not want the overhead of a Handler, or you do not want the actual dynamic script reference, then there is nothing stopping you in cutting this write down, and making a HtmlHelper which simply parsing a type and outputs the JavaScript variables directly to the requesting resource, so instead of an include script tag, it would output a script block directly in the dom.  Personally I just like the script tag and the visual reduction in code in the view source, I am unsure of any performance gain if any. 

So that is basically it, this is something I am definitely going to test drive and among other things, use it for other purposes.  One idea I had was to use this to generate Client Side objects based on say the models. I will do this for the next post I hope.



.NET | C# | JavaScript
Sunday, December 13, 2009 11:53:19 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer

This plug-in I made is quite handy and has a few applications.  I think JQuery is a fantastic library for javascript, as it does what it says on the tin:

WRITE LESS, DO MORE

Ok so the fader, I have written this plugin with a few options, which are:

  • slideClass
    • This is a JQuery selector syntax
  • slideDuration
    • This duration that the currently item stays visible for
  • fadeOutDuration
    • The duration it takes for an item to fade out
  • fadeInDuration
    • The duration it takes for an item to fade in

The structure which the plugin expects is simply a container element e.g. a div and subsequent nested containers which it will apply its effect to:

    <div class="slides">
        <div class="slide">
            <h1>Slide 1</h1>
        </div>
        <div class="slide">
            <h1>Slide 2</h1>
        </div>
        <div class="slide">
            <h1>Slide 3</h1>
        </div>
        <div class="slide">
            <h1>Slide 4</h1>
        </div>
    </div>

To trigger this function as soon as possible I place the following just before the end of the body tag:

...  
	<script type="text/javascript">
        (function() {
            var options = {
                slideClass: ".slide",
                slideDuration: 6000,
                fadeOutDuration: 2000,
                fadeInDuration: 500
            };

            $(".slides").fader(options);
        })();
    </script>
</body>

So if we look at the above HTML structure inline with the js, you can see the selector of “.slide” which tells the plugin which items to fade in/out.  You can apply this to many different containers with different options, the below shows an example of this with different speeds.  The html markup is as follows:

    <div class="slides">
        <div class="slide">
            <h1>Slide 1</h1>
        </div>
        <div class="slide">
            <h1>Slide 2</h1>
        </div>
        <div class="slide">
            <h1>Slide 3</h1>
        </div>
        <div class="slide">
            <h1>Slide 4</h1>
        </div>
    </div>
    
   <div class="slides2">
        <div class="slide">
            <h1>Slide 1</h1>
        </div>
        <div class="slide">
            <h1>Slide 2</h1>
        </div>
        <div class="slide">
            <h1>Slide 3</h1>
        </div>
        <div class="slide">
            <h1>Slide 4</h1>
        </div>
    </div>

This time I create a second container of class “slides2” but keep the containing elements with class of “slide.” To start the plugin on both containers I add another instance of options and select the second container.

...
	<script type="text/javascript">
        (function() {
            var options = {
                slideClass: ".slide",
                slideDuration: 6000,
                fadeOutDuration: 2000,
                fadeInDuration: 500
            };

            var options2 = {
                slideClass: ".slide",
                slideDuration: 2000,
                fadeOutDuration: 500,
                fadeInDuration: 500
            };

            $(".slides").fader(options);
            $(".slides2").fader(options2);
        })();
    </script>
</body>

The Plug-in Code

jQuery.fn.fader = function(options) {
    if (!options.slideClass || options.slideClass == "") {
        var error = new Error();
        error.message = "No slide class has been supplied";
    }

    if (!options.slideDuration || !parseInt(options.slideDuration)) {
        options.slideDuration = 4000;
    }

    if (!options.fadeOutDuration || !parseInt(options.fadeOutDuration)) {
        options.fadeOutDuration = 4000;
    }


    if (!options.fadeInDuration || !parseInt(options.fadeInDuration)) {
        options.fadeInDuration = 4000;
    }

    return this.each(function() {
        $(options.slideClass, this).hide();
        $(options.slideClass + ":eq(0)", this).show();
        var currentIndex = 0;
        var obj = this;
        var t = setInterval(function() {
        $(options.slideClass + ":eq(" + currentIndex + ")", obj).fadeOut(options.fadeOutDuration, function() {
                currentIndex++;
                if (currentIndex > $(options.slideClass, obj).size() - 1)
                    currentIndex = 0;
                $(options.slideClass + ":eq(" + currentIndex + ")", obj).fadeIn(options.fadeInDuration);
            });
        }, options.slideDuration);
    });
};

Sunday, March 15, 2009 9:57:44 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer

A question recently on StackOverflow.com triggered me to think that it would be nice if in JavaScript I could do this:

var d = new Date();
d.AddDays(1);

So exactly like what I can do in .NET.  I know a lot of people try and achieve date solutions by concatenating strings and parsing as a date, but this is not an option for me as JavaScript’s native DateTime is very powerful and due to this, there is absolutely no need to be hashing together strings.  So I will now paste the functionailty in C# that I based my methods on, dead simple, it is:

    DateTime d = new DateTime();

    public void Page_Load(object sender, EventArgs e)
    {
        DateTime days = d.AddDays(1);
        DateTime hours = d.AddHours(1);
        DateTime milliseconds = d.AddMilliseconds(1);
        DateTime minutes = d.AddMinutes(1);
        DateTime months = d.AddMonths(1);
        DateTime seconds = d.AddSeconds(1);
    }
So to exploit another JavaScript feature I will use prototype to extend the methods of the Date type inside javascript.  C# now has extension methods which are quite similar in what they allow you to use them like.  So the prototypes I want to create are as follows:

  • AddDays
  • AddHours
  • AddMilliseconds
  • AddMinutes
  • AddMonths
  • AddSeconds
    <script type="text/javascript">

        Date.prototype.AddDays = function(days) {
            this.setDate(this.getDate() + days);
            return this;
        }

        Date.prototype.AddHours = function(hours) {
            this.setHours(this.getHours() + hours);
            return this;
        }

        Date.prototype.AddMilliseconds = function(milliseconds) {
            this.setMilliseconds(this.getMilliseconds() + milliseconds);
            return this;
        }

        Date.prototype.AddMinutes = function(minutes) {
            this.setMinutes(this.getMinutes() + minutes, this.getSeconds(), this.getMilliseconds());
            return this;
        }

        Date.prototype.AddMonths = function(months) {
            this.setMonth(this.getMonth() + months, this.getDate());
            return this;
        }

        Date.prototype.AddSeconds = function(seconds) {
            this.setSeconds(this.getSeconds() + seconds, this.getMilliseconds());
            return this;
        }

        Date.prototype.AddYears = function(years) {
            this.setFullYear(this.getFullYear() + years);
            return this;
        }
    
    </script>

Excellent, nice clean, precise code.  So with this I can now do the following:

        <script type="text/javascript">
            var d = new Date();
            
            var output = d.AddDays(1).toString() + "<br/>" +
            d.AddHours(1).toString() + "<br/>" +
            d.AddMilliseconds(1).toString() + "<br/>" +
            d.AddMinutes(1).toString() + "<br/>" +
            d.AddMinutes(1).toString() + "<br/>" +
            d.AddMonths(1).toString() + "<br/>" +
            d.AddSeconds(1).toString() + "<br/>" +
            d.AddYears(1).toString() + "<br/>";

            document.write(output);
        
        </script>

With the output of

Sat Mar 07 2009 08:41:09 GMT+0000 (GMT Standard Time)
Sat Mar 07 2009 09:41:09 GMT+0000 (GMT Standard Time)
Sat Mar 07 2009 09:41:09 GMT+0000 (GMT Standard Time)
Sat Mar 07 2009 09:42:09 GMT+0000 (GMT Standard Time)
Sat Mar 07 2009 09:43:09 GMT+0000 (GMT Standard Time)
Tue Apr 07 2009 09:43:09 GMT+0100 (GMT Daylight Time)
Tue Apr 07 2009 09:43:10 GMT+0100 (GMT Daylight Time)
Wed Apr 07 2010 09:43:10 GMT+0100 (GMT Daylight Time)

Obviously if you want to subtract values from a date you would provide negative values into the methods, i.e. DateTime.AddDays(-10);

Cheers for now,

Andy


Friday, March 06, 2009 8:50:42 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer

I think I am thinking too much about how to write a good blog post.  I think I need to master letting my brain flow while intercepting the output and tweaking things.  Well I have not yet mastered it, but I shall continue to try.  I was asked today to create a simple JQuery plug in which showed an html element containing a links title and this should be able to be styled and also follow the mouse whilst in hover state.  It is kinda like a pimped up tooltip.  The only thing out of the ordinary I did was hijack the rel attribute of the link as, if I continued to use the title attribute I got the default browser tooltip which overlaid my DHTML div.  I was thinking about creating my own attribute to add to the tag, but then again this would probably fail XHTML Validation.  This makes me wonder because from what I have read the new and upcoming version of the ASP.NET Ajax library introduce different attributes to the HTML.  Not sure if this is due to be released with the next iteration of the HTML standard.

Write to my plug in.  Here is the actual code I wrote for the plug in itself:

        jQuery.fn.popupLinkHelper = function(options) {
            var element = document.createElement("div");
            $(element).addClass(options.popupCssClass).hide();
            document.body.appendChild(element);

            return this.each(function() {
                $(this).hover(function() {
                    $(element).show();
                    $(element).html($(this).attr("rel"));
                    $(this).mousemove(function(e) {
                        $(element).css({
                            "position": "absolute",
                            "top": e.pageY + options.offsetY,
                            "left": e.pageX + options.offsetX
                        });
                    });
                }, function() {
                    $(element)
                });
            });
        };

To give an example using CSS I used the following CSS class to spruce up the popup:

        .popup
        {
            width: 150px;
            padding: 10px;
            background-color: #3366CC;
            border: black 2px solid;
            color: White;
        }

To trigger this code I use a notation publicised on the JQuery plug in authoring page.  As well I threw in some configuration options to as to be able to have more flexibility with it in the future:

    <script type="text/javascript">
        $(function() {
            var options = {
                offsetX: 50,
                offsetY: 0,
                popupCssClass: "popup"
            };
            $("a.pol").popupLinkHelper(options);
        });
    </script>

</body>

To test the following out I simply created a ul li block using the class pol (Pop out link – :-P) to identify those which I would target with the plug in.

    <ul>
        <li><a href="#" rel="This is example 1" class="pol">Example Link 1</a></li>
    </ul>

And the result:

image

Cheers for now:

Andrew,

P.S. Cross Browser Transparent Rounded Corners which is compatible with IE 6 – This is like Rocking Horse Muck, so hard to find lol.


Thursday, March 05, 2009 8:09:05 PM (GMT Standard Time, UTC+00:00)  #    Disclaimer

I had a situation recently where:

  1. I needed a cascading drop down interface
  2. I needed it to execute on the client
  3. I needed to reload the parent and child items' selection.

The Cascading DropDownList Extender is an excellent control as are its siblings inside the AjaxControlToolkit.  At first I was scouring the outputted JavaScript that it uses inline with the ServiceMethod to find a method by which I could programmatically set the selected index, which in turn should trigger the data retrieval.

"All other things being equal, the simplest solution is the best."

Occam's razor

It was at this point I saw that the Cascading DropDownList extender actually has a SelectedValue property.  I could have kicked myself when i saw it, as really I should have researched the control more and its capabilities and limitations.  Ever heard of the 7 P's - British SAS lol ?

So I integrated it with a control I built.  The control was a Region, County, TownCity and Area selection in respective order, so as to bring more structure to clients addresses for a project I am currently on.  Similar to a previous post where I create a composite control I declare the control extenders inside this custom composite control and control which I render dependant on other properties which are set. 

        private void CreateRegion()
        {
            DropDownListRegion = new DropDownList();
            DropDownListRegion.ID = this.ID + "_DropDownListRegion";
            CascadingDropDownRegion = new CascadingDropDown();
            CascadingDropDownRegion.ID = this.ID + "_CascadingDropDownRegion";
            CascadingDropDownRegion.Category = REGION_CATEGORY;
            CascadingDropDownRegion.EmptyText = REGION_EMPTYTEXT;
            CascadingDropDownRegion.EmptyValue = "0";
            CascadingDropDownRegion.LoadingText = REGION_LOADINGTEXT;
            CascadingDropDownRegion.PromptText = REGION_PROMPTTEXT;
            CascadingDropDownRegion.PromptValue = "0";
            CascadingDropDownRegion.ServiceMethod = REGION_SERVICE_METHOD;
            CascadingDropDownRegion.ServicePath = SERVICE_PATH;
            CascadingDropDownRegion.TargetControlID = DropDownListRegion.ID;
            if (RegionID != 0)
                CascadingDropDownRegion.SelectedValue = RegionID.ToString();
            Controls.Add(DropDownListRegion);
            Controls.Add(CascadingDropDownRegion);


            if (UseAddressLevel == AddressLevel.Region)
            {
                RequiredFieldValidatorRegion = new RequiredFieldValidator();
                RequiredFieldValidatorRegion.ID = this.ID + "_RequiredFieldValidatorRegion";
                RequiredFieldValidatorRegion.ControlToValidate = DropDownListRegion.ID;
                RequiredFieldValidatorRegion.InitialValue = "0";

                ValidatorCalloutExtenderRegion = new ValidatorCalloutExtender();
                ValidatorCalloutExtenderRegion.ID = this.ID + "_ValidatorCalloutExtenderRegion";
                ValidatorCalloutExtenderRegion.TargetControlID = RequiredFieldValidatorRegion.ID;

                Controls.Add(RequiredFieldValidatorRegion);
                Controls.Add(ValidatorCalloutExtenderRegion);
            }
        }

image

So the whole thing above is a control, but the amount of collapsible extenders displayed is control by an enum called AddressLevel.  How it works is simply like this, if you only require the Town and City, it is this level with which you set it, what happens then is that the area will not be displayed and also the TownAndCity will gain validation.

Line 16 of the code above will set selected index based on whether the RegionID has been set.  I find it so handy and useful that I can strongly type my .NET controls to work with the control extenders as it just adds a greater level to the UI experience.  Strongly Typing Javascript is also a good IDEA and something which I want to touch on in later posts aswell.

Cheers,

Andrew

P.S. setting the selected index for multiple cascading dropdownlists will work so its parent gets populated and set's the selected value using client side code, the child cascading dropdownlist will wait for this event, then populate and THEN check for the presence of the selected value in its items and again use client side to set the selected index.  After that it CASCADES!! lol



AJAX | ASP.NET | C# | JavaScript
Tuesday, January 27, 2009 11:07:54 AM (GMT Standard Time, UTC+00:00)  #    Disclaimer