Friday, April 30, 2010

MVC extensions: Cascading dropdown control

In my previous post here, I explained how to implement cascading dropdown by adding additional html custom attributes and use it to populate the sibling dropdown with the help of jQuery. I also showed a sample code of new extension control “CascadeDropDown” that will help us to avoid the hassle of writing these attributes manually.   

Today I will continue with the CascadeDropDown control to implement the view data binding, this is important for populating the top level cascade dropdown from the view data in the same manner as the original MVC htmlhelper dropdown.

/// <summary>
/// Render cascading dropdown control 
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="name">Rendered html element name</param>
/// <param name="defaultText">The default initial text to display at top</param>
/// <param name="actionName">The name of the action used to fill the sibling dropdown</param>
/// <param name="controllerName">The action controller name of the action used to fill the sibling dropdown</param>
/// <param name="siblingControlId">The sibling dropdown Id to fill on selected item change</param>
/// <param name="siblingdefaultText">The sibling dropdown default text</param>
/// <param name="siblingdefaultValue">The sibling dropdown default value</param>
/// <returns>string</returns>
public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string defaultText, string actionName, string controllerName, string siblingControlId, string siblingdefaultText, string siblingdefaultValue)
{  
    var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
    string action = urlHelper.Action(actionName, controllerName, null);
    StringBuilder sb = new StringBuilder();
    TagBuilder inputBuilder = new TagBuilder("select");
    inputBuilder.GenerateId(name);
    inputBuilder.MergeAttribute("name", name);
    inputBuilder.MergeAttribute("dataurl", action);
    inputBuilder.MergeAttribute("targetcontrolid", siblingControlId);
    if (siblingdefaultText != null) inputBuilder.MergeAttribute("deftext", siblingdefaultText);
    if (siblingdefaultValue != null) inputBuilder.MergeAttribute("defvalue", siblingdefaultValue);
    if (htmlHelper.ViewData.ContainsKey(name))
    {
        sb.Append(inputBuilder.ToString(TagRenderMode.StartTag));
        
        SelectList data = (SelectList)htmlHelper.ViewData[name];
        if (defaultText != null) sb.AppendFormat("<option value=\"\">{0}</option>", defaultText);
        foreach (SelectListItem item in data)
        {
            if (item.Selected)
            {
                sb.AppendFormat("<option value=\"{0}\" selected>{1}</option>", item.Value, item.Text);
            }
            else
            {
                sb.AppendFormat("<option value=\"{0}\">{1}</option>", item.Value, item.Text);
            }
        }
        sb.Append(inputBuilder.ToString(TagRenderMode.EndTag));
    }
    else
    {
        inputBuilder.MergeAttribute("disabled", "disabled");
        sb.Append(inputBuilder.ToString(TagRenderMode.SelfClosing));
    }
    
    return sb.ToString();        
}

Now we need to create a select list and set it in the View Data [with the same name as the cascade dropdown].

ViewData["Countries"] = new SelectList(context.Country, "CountryId", "CountryName");
 

Note: MVC extension Html helpers must be implemented in a static class. 

Thursday, April 29, 2010

Cascading dropdown using jQuery and MVC framework

My page contains 10 cascading dropdowns, so instead of write a JavaScript code to handle each dropdown which need a lot of copy and paste and at the end it will look ugly and hard to maintain. So my idea was to write a simple JavaScript code once included and initiated in my page it will do the work for me.

The idea was to add custom html attributes in the dropdown to specify which control will be affected when the current selected value change and the data source to populate the sibling control from.

First let’s start with the required attributes we need to be able to populate the sibling dropdown when the current one change 

function cascadeDropDown() {
    this.dataUrl;              // the action name to use when populate the child dropdown 
    this.targetcontrolId;      // the dropdown Id that will be populated on change
    this.defText;              // the default text displayed after data population 
    this.defValue;             // the default selected value
}

Then we will add the equivalent attribute in the dropdown html, In our example we will populate the states dropdown based on the selected country.

<select name="Country" id="Country" targetcontrolid="States"
        dataurl="/Admin/JsonStatesCombo" deftext="Select State">
 
<select name="States" id="States" disabled="disabled"></select>
}

 

Next we need to read these attribute in the Country select box and use it to populate the States select box.

function initCascadeDropDowns() {
    $(document).ready(function() {
        $('select').each(function() {
            // Read html attribute and set the cascadeDropDown object properties 
            var dropdown = new cascadeDropDown();
            dropdown.targetcontrolId = $(this).attr('targetcontrolid');
            dropdown.dataUrl = $(this).attr('dataurl');
            dropdown.defValue = $(this).attr('defvalue');
            dropdown.defText = $(this).attr('deftext');
            // Check for min required attribute to populate the next dropdown successfully. 
            if (dropdown.targetcontrolId != null && dropdown.dataUrl != null) {
                // attach onchange event to the current select box if needed attributes found
                $(this).change(function() {
                    if ($(this).val() == '' || $('option', this).size() == 0) {
                        // clear the child dropdowns data by recursively trigger each child onchange event.
                        $('#' + dropdown.targetcontrolId).trigger("change");
                        $('#' + dropdown.targetcontrolId).empty();
                        $('#' + dropdown.targetcontrolId).attr("disabled", "disabled");
                    }
                    else {
                        // populate child dropdown data
                        FillInCascadeDropdown({ parentVal: $(this).find(":selected").val() },
                        '#' + dropdown.targetcontrolId, dropdown.dataUrl, dropdown.defText, dropdown.defValue);
                    }
                });
            }
        });
    });    
}

And here is the implementation of the FillInCascadeDropdown function

function FillInCascadeDropdown(map, dropdown, action, defText, defValue) {
    $(dropdown).empty();
    if (defText != null) $(dropdown).append("<option value=\"\">" + defText + "</option>");
    $.getJSON(action, map,
              function(result) {
                  for (i = 0; i < result.length; i++) {
                      if (result[i].Selected) $(dropdown).append("<option value=\"" + result[i].Value + "\" SELECTED>" + result[i].Text + "</option>");
                      else $(dropdown).append("<option value=\"" + result[i].Value + "\">" + result[i].Text + "</option>");
                  }
                  if (result.length > 0)
                      $(dropdown).removeAttr('disabled');
                  else {
                      $(dropdown).trigger("change");
                      $(dropdown).empty();
                      $(dropdown).attr("disabled", "disabled");
                  }
                  if (defValue == null && defText != null)
                      defValue == "";
                  $(dropdown).val(defValue);
              });
          }
 

 

We are almost done,  we just need to implement the JsonstatesCombo action in the Admin Controller.

[AcceptVerbs(HttpVerbs.Get)]
public JsonResult JsonStatesCombo(int parentVal)
{
   LinqContext context = new LinqContext();
   var model = from s in context.States where CountryId == parentVal
                    select s;
   var list = new SelectList(model, "StateId", "StateName"); 
   return Json(list);
}

 

I included the JavaScript above in external js file so it can be used in many pages as we want, Now when we need to work with cascade dropdowns all we just need to call initCascadeDropDowns() when document is loading after attach the proper html attributes.

 

MVC Extensions 

Need to make my life easier and my html code to be more elegant and that’s not everything, also it will be great to let Visual studio IntelliSense help me while writing the html attributes and not to forget required attribute or mistype the attribute name.

Here MVC extensions become handy, I will add new html helper control and call it CascadeDropDown

public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId)
{
    return CascadeDropdown(htmlHelper, name, actionName, controllerName, targetControlId, null, null);
}
 
public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId, string defaultText)
{
    return CascadeDropdown(htmlHelper, name, actionName, controllerName, targetControlId, defaultText, null);
}
 
public static string CascadeDropdown(this HtmlHelper htmlHelper, string name, string actionName, string controllerName, string targetControlId, string defaultText, string defaultValue)
{
    var urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);
    string action = urlHelper.Action(actionName, controllerName, null);
    TagBuilder inputBuilder = new TagBuilder("select");
    inputBuilder.MergeAttribute("dataurl", action);
    inputBuilder.MergeAttribute("targetcontrolid", targetControlId);
    if (defaultText != null) inputBuilder.MergeAttribute("deftext", defaultText);
    if (defaultValue != null) inputBuilder.MergeAttribute("defvalue", defaultValue);
    inputBuilder.MergeAttribute("name", name);
    inputBuilder.MergeAttribute("disabled", "disabled");
    inputBuilder.GenerateId(name);
    return inputBuilder.ToString(TagRenderMode.SelfClosing);
}

 

Now we replace the old select html tag with CascadeDropDown control.

<!--<select name="Country" id="Country" disabled="disabled" targetcontrolid="States"
dataurl="/Admin/JsonStatesCombo" deftext="Select State"></select> -->
 
<%
 = Html.CascadeDropdown("Country", "JsonStatesCombo", "Admin", "States", "Select State") 
%>
                

 

Update: In a later post i extended the cascade dropdown control to support view server binding.

Sunday, April 25, 2010

How to make “LINQ to SQL” in a Class library refer to web.config connectionstring?

Creating LINQ to SQL classes in a DAL class library and reference it in a test project then publish the application on testing server, update the connection string with the testing server setting.

i try to open the test page which consume my “LINQ to SQL” classes, i got the error page say that the SQL Server doesn’t exist!! Luckily I have a login page using membership provider and it is working properly then the Server exists and the connection string is correct (Saved my day) so my problem is in the LINQ to SQL classes.

I found an easy way to fix this by passing the connection string in the LINQ context like this          

using(TestProjectDataContext context = new TestProjectDataContext(ConfigurationManager.ConnectionStrings[0].ConnectionString))
{
    // Implementation here
}

Since this is a test project it was easy to change, I didn’t know why it work fine locally without passing the connection string and fail on the hosting server!!

Later i discovered that this can be fixed by changing the web.config connectionstring name to a fully qualified one like this

<connectionStrings>
        <!-- 
            This one will not work for LINQ to SQL
            <add name="TestProjectConnectionString"
            connectionString="Data Source=.;Initial Catalog=IslamDB;Integrated Security=True"
            providerName="System.Data.SqlClient" /> 
        -->
            <add name="TestProject.DAL.Properties.Settings.TestProjectConnectionString"
            connectionString="Data Source=.;Initial Catalog=IslamDB;Integrated Security=True"
            providerName="System.Data.SqlClient" />
    </connectionStrings>
 

I like it that way