Friday, February 25, 2011

jqGrid - customizing the multi-select option (restrict single selection and adding custom events)

Goal:

Using the jgGrid to enable a selection of a checkbox for row selection - which is easy to set in the jqGrid - but also only allowing a single row to be selectable at a time while adding events based on whether the row was selected or de-selected.

Environment:

Issue:

The jqGrid does not support the option to restrict the multi-select to only allow for a single selection. You may ask, why bother with the multi-select checkbox function if you only want to allow for the selection of a single row? Good question, as an example, you want to reserve the selection of a row to trigger another kind of event and use the checkbox multi-select to handle a different kind of event; in other words, when I select the row I want something entirely different to happen than when I select to check off the checkbox for that row.
Also the setSelection method of the jqGrid is a toggle and has no support for determining whether the checkbox has already been selected or not, So it will simply act as a switch - which it is designed to do - but with no way out of the box to only check off the box (as in not to de-select) rather than act like a switch.
Furthermore, the getGridParam('selrow') does not indicate if the row was selected or de-selected, which seems a bit strange and is the main reason for this blog post.

Solution:

How this will act:

When you check off a multi-select checkbox in the gird, and then commence to select another row by checking off that row's multi-select checkbox - I'm not talking there about clicking on the row but using the grid's multi-select checkbox - it will de-select the previous selection so that you are always left with only a single selection.
Furthermore, once you select or de-select a multi-select checkbox, fire off an event that will be determined by whether or not the row was selected or de-selected, not just merely clicked on. So if I de-select the row do one thing but when selecting it do another.

Implementation (this of course is only a partial code snippet):

            multiselect: true,
            multiboxonly: true,
            onSelectRow: function (rowId) {
                var gridSelRow = $(item).getGridParam('selrow');
                var s;
                s = $(item).getGridParam('selarrrow');
                if (!s || !s[0]) {
                    $(item).resetSelection();
                    $('#productLineDetails').fadeOut();
                    lastsel = null;
                    return;
                }
                var selected = $.inArray(rowId, s) != -1;
                if (selected) {
                    $('#productLineDetails').show();
                }
                else {
                    $('#productLineDetails').fadeOut();
                }

                if (rowId && rowId !== lastsel && selected) {
                    $(item).GridToForm(gridSelRow, '#productLineDetails');
                    if (lastsel) $(item).setSelection(lastsel, false);
                }
                lastsel = rowId;
            },

In the example code above:

The "item" property is the id of the jqGrid.
The following to settings ensure that the jqGrid will add the new column to select rows with a checkbox and also the not allow for the selection by clicking on the row but to force the user to have to click on the multi-select checkbox to select the row:
multiselect: true,

multiboxonly: true,
Unfortunately the var gridSelRow = $(item).getGridParam('selrow') function will only return the row the user clicked on or rather that the row's checkbox was clicked on and NOT whether or not it was selected nor de-selected, but it retrieves the row id, which is what we will need.
The following piece get's all rows that have been selected so far, as in have a checked off multi-select checkbox:
var s;

s = $(item).getGridParam('selarrrow');
Now determine if the checkbox the user just clicked on was selected or de-selected:
var selected = $.inArray(rowId, s) != -1;
If it was selected then show a container "#productLineDetails", if not hide that container away.
The following instruction populates a form with the grid data using the built-in GridToForm method (just mentioned here as an example) ONLY if the row has been selected and NOT de-selected but more importantly to de-select any other multi-select checkbox that may have been selected:
if (rowId && rowId !== lastsel && selected) {
                    $(item).GridToForm(gridSelRow, '#productLineDetails');
                    if (lastsel) $(item).setSelection(lastsel, false);
}

Thursday, February 17, 2011

Menu widget - no jQuery nor Javascript required - pure CSS

Goal:

Create a menu widget that does not require any javascript, extremely lightweight, very fast, soley based on CSS, compatible with FireFox and Chrome.
Issues:

May have some rendering issues in some versions of IE, sorry :-)
Instruments:

* css file
* html with specific menu format
* jQuery-ui library - optional if you want to use your own images/colors

Implementation Details:
HTML:

CSS:

/* =Menu
----------------------------------------------------------------------------------------- */
#header #header_Menubar
{
margin: 0;
padding: 0;
border: 0;
width: 100%;
height: 22px;
}

#header
{
background-color: #99cccc;
background-color: #aaccee;
background-color: #5BA3E0;
background-color: #006cb1;

}

/* Set menu bar background color */
#header #header_Menubar
{
background-attachment: scroll;
background-position: left center;
background-repeat: repeat-x;
}

/* Set main (horizontal) menu typology */
#header .linkList0
{
padding: 0 0 1em 0;
margin-bottom: 1em;
font-family: 'Trebuchet MS', 'Lucida Grande',
Verdana, Lucida, Geneva, Helvetica,
Arial, sans-serif;
font-weight: bold;
font-size: 1.085em;
font-size: 1em;
}

/* Set all ul properties */
#header .linkList0, #header .linkList0 ul
{
list-style: none;
margin: 0;
padding: 0;
list-style-position: outside;
}

/* Set all li properties */
#header .linkList0 > li
{
float: left;
position: relative;
font-size: 90%;
margin: 0 0 -1px;
width: 9.7em;
padding-right: 2em;
z-index: 100; /*IE7: Fix for IE7 hiding drop down list behind some other page elements */
}

/* Set all li properties */
#header .linkList01 > li
{
width: 190px;
}

#header .linkList0 .linkList01 li
{
margin-left: 0px;
}

/* Set all list background image properties */
/*#header .linkList0 li a
{
background-position: left center;
background-image: url( '../Content/Images/VerticalButtonBarGradientFade.png' );
background-repeat: repeat-x;
background-attachment: scroll;
}*/
/* Set all A ancor properties */
#header .linkList0 li a
{
display: block;
text-decoration: none;
line-height: 22px;
}

/* IE7: Fix for a bug in IE7 where the margins between list items is doubled - need to set height explicitly */
*+html #header .linkList0 ul li
{
height: auto;
margin-bottom: -.3em;
}

/* Menu: Set different borders for different nested level lists
-------------------------------------------------------------- */
#header .linkList0 > li a
{
border-left: 10px solid Transparent;
border-right: none;
}

#header .linkList0 > li a
{
border-left: 0px;
margin-left: 0px;
border-right: none;
}

#header .linkList0 .linkList01 > li a
{
border-left: 8px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

#header .linkList0 .linkList01 .linkList001 > li a
{
border-left: 6px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

#header .linkList0 .linkList01 .linkList001 .linkList0001 > li a
{
border-left: 4px solid #336699;
border-right: none;
border: 1px solid Transparent;
-moz-border-radius: 5px 5px 5px 5px;
-moz-box-shadow: 3px 3px 4px #696969;
}

/* Link and Visited pseudo-class settings for all lists (ul) */
#header .linkList0 a:link, #header .linkList0 a:visited
{
display: block;
text-decoration: none;
padding-left: 1em;
}

/* Hide all the nested/sub menu items */
#header .linkList0 ul
{
display: none;
padding: 0;
position: absolute; /*Important: must not impede on other page elements when drop down opens up */
}

/* Hide all detail popups */
#header .detailPopup
{
display: none;
}

/* Set the typology of all sub-menu list items li */
/*#header .linkList0 ul li
{
background-color: #AACCEE;
background-position: left center;
background-image: url( '../Content/Images/VerticalButtonBarGradientFade.png' );
background-repeat: repeat-x;
background-attachment: scroll;
}*/

#header .linkList0 ul li.more
{
background: Transparent url('../Content/Images/ArrowRight.gif') no-repeat right center;
}

/* Header list's margin and padding for all list items */
#header .linkList0 ul li
{
margin: 0 0 0 1em;
padding: 0;
}

#header .linkList01 ul li
{
margin: 0;
padding: 0;
width: 189px;
}

/* Set margins for the third li sibling (Plan a Call) to display to the right of the parent menu
to avoid the sub-menu overlaying the menu items below */
#header .linkList0 li.more .linkList01 li.more > ul.linkList001
{
margin: -1.7em 0 0 13.2em; /*Important, must be careful, if tbe EM since gap increases too much bewteen nested lists the gap will make the nested-list collapse prematurely */
}

/* Set right hand arrow for list items with sub-menus (class-more) */
#header li.more
{
background: Transparent url('../Content/Images/ArrowRight.gif') no-repeat right center;
padding-right: 48px;
}

/* Menu: Dynamic Behavior of menu items (hover, visted, etc)
----------------------------------------------------------- */
#header .linkList0 li a:link, #header .linkList01 li a:link
{
display: block;
}

#header .linkList0 li a:visited, #header .linkList01 li a:visited
{
display: block;
}

#header .linkList0 > li:hover
{
}

#header .linkList01 > li:hover a
,#header .linkList001 > li:hover a
{
text-decoration: underline;
}

#header .linkList0 > li abbr:hover span.detailPopup
{
display: block;
position: absolute;
top: 1em;
left: 17em;
border: double 1px #696969;
border-style: outset;
width: 120%;
height: auto;
padding: 5px;
font-weight: 100;
}

#header .linkList0 > li:hover
,#header .linkList0 .linkList01 > li:hover
{
}

#header .linkList0 .linkList01 .linkList001 > li:hover
{
}

#header .linkList0 .linkList01 .linkList001 .linkList0001 > li:hover
{
}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 li:hover > ul
{
display: block;
}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 .linkList01 li:hover > ul
{
display: block;
background: -moz-linear-gradient(top, #1E83CC, #619FCD);
/* Chrome, Safari:*/
background: -webkit-gradient(linear,
center top, center bottom, from(#1E83CC), to(#619FCD));

}

/* Display the hidden sub menu when hovering over the parent ul's li */
#header .linkList0 .linkList01 .linkList001 li:hover > ul
{
display: block;
}

/* Set right hand arrow for list items with sub-menus (class-more) on hover */
#header li.more:hover
{
}

Also some CSS for global settings that will affect this menu, you of course will have some other styling, but included it here so you can see how/why some css properties were set here:

/* Neutralize styling:
Elements we want to clean out entirely: */
html, body
{
margin: 0;
padding: 0;
font: 62.5%/120% Verdana, Arial, Helvetica, sans-serif;
}

/* Neutralize styling:
Elements with a vertical margin: */
h1, h2, h3, h4, h5, h6, p, pre,
blockquote, ul, ol, dl, address {
margin: 0; /* most browsers set some default value that is not shared by all browsers */
padding: 0; /* some borowsers default padding, set to 0 for all */
}

/* Apply left margin:
Only to the few elements that need it: */
li, dd, blockquote {
margin-left: 1em;
}

Thursday, February 10, 2011

jqgrid - dynamically load different drop down values for different rows depending on another column value

Goal:

As we all know the jqGrid examples in the demo and the Wiki always refer to static values for drop down boxes. This of course is a personal preference but in dynamic design these values should be populated from the database/xml file, etc, ideally JSON formatted.
Can you do this in jqGrid, yes, but with some custom coding which we will briefly show below (refer to some of my other blog entries for a more detailed discussion on this topic).
What you CANNOT do in jqGrid, referring here up and to version 3.8.x, is to load different drop down values for different rows in the jqGrid. Well, not without some trickery, which is what this discussion is about.

Issue:

Of course the issue is that jqGrid has been designed for high performance and thus I have no issue with them loading a  reference to a single drop down values list for every column. This way if you have 500 rows or one, each row only refers to a single list for that particular column. Nice!
SO how easy would it be to simply traverse the grid once loaded on gridComplete or loadComplete and simply load the select tag's options from scratch, via Ajax, from memory variable, hard coded etc? Impossible! Since their is no embedded SELECT tag within each cell containing the drop down values (remember it only has a reference to that list in memory), all you will see when you inspect the cell prior to clicking on it, or even before and on beforeEditCell, is an empty .
When trying to load that list via a click event on that cell will temporarily load the list but jqGrid's last internal callback event will remove it and replace it with the old one, and you are back to square one.

Solution:

Yes, after spending a few hours on this found a solution to the problem that does not require any updates to jqGrid source code, thank GOD!
Before we get into the coding details, the solution here can of course be customized to suite your specific needs, this one loads the entire drop down list that would be needed across all rows once into global variable. I then parse this object that contains all the properties I need to filter the rows depending on which ones I want the user to see based off of another cell value in that row. This only happens when clicking the cell, so no performance penalty. You may of course to load it via Ajax when the user clicks the cell, but I found it more efficient to load the entire list as part of jqGrid's normal editoptions: { multiple: false, value: listingStatus } colModel options which again keeps only a reference to the single list, no duplication.
Lets get into the meat and potatoes of it.
        var acctId = $('#Id').val();

        var data = $.Ajax({ url: $('#ajaxGetAllMaterialsTrackingLookupDataUrl').val(), data: { accountId: acctId }, dataType: 'json', async: false, success: function(data, result) { if (!result) alert('Failure to retrieve the Alert related lookup data.'); } }).responseText;
        var lookupData = eval('(' + data + ')');

        var listingCategory = lookupData.ListingCategory;

        var catList = '{';
        $(lookupData.ListingCategory).each(function() {
            catList += this.Id + ':"' + this.Name + '",';
        });
        catList += '}';



        var lastsel;
        var ignoreAlert = true;
        $(item)
        .jqGrid({
            url: listURL,
            postData: '',
            datatype: "local",
            colNames: ['Id', 'Name', 'Commission
Rep', 'Business
Group', 'Order
Date', 'Edit', 'TBD', 'Month', 'Year', 'Week', 'Product', 'Product
Type', 'Online/
Magazine', 'Materials', 'Special
Placement', 'Logo', 'Image', 'Text', 'Contact
Info', 'Everthing
In', 'Category', 'Status'],
            colModel: [
                { name: 'Id', index: 'Id', hidden: true, hidedlg: true },
                { name: 'AccountName', index: 'AccountName', align: "left", resizable: true, search: true, width: 100 },
                { name: 'OnlineName', index: 'OnlineName', align: 'left', sortable: false, width: 80 },
                { name: 'ListingCategoryName', index: 'ListingCategoryName', width: 85, editable: true, hidden: false, edittype: "select", editoptions: { multiple: false, value: eval('(' + catList + ')') }, editrules: { required: false }, formatoptions: { disabled: false} }

            ],
            jsonReader: {
                root: "List",
                page: "CurrentPage",
                total: "TotalPages",
                records: "TotalRecords",
                userdata: "Errors",
                repeatitems: false,
                id: "0"
            },
            rowNum: $rows,
            rowList: [10, 20, 50, 200, 500, 1000, 2000],
            imgpath: jQueryImageRoot,
            pager: $(item + 'Pager'),
            shrinkToFit: true,
            width: 1455,
            recordtext: 'Traffic lines',
            sortname: 'OrderDate',
            viewrecords: true,
            sortorder: "asc",
            altRows: true,
            cellEdit: true,
            cellsubmit: "remote",
            cellurl: editURL + '?rows=' + $rows + '&page=1',
            loadComplete: function() {

            },
            gridComplete: function() {

            },
            loadError: function(xhr, st, err) {

            },
            afterEditCell: function(rowid, cellname, value, iRow, iCol) {
                var select = $(item).find('td.edit-cell select');
                $(item).find('td.edit-cell select option').each(function() {
                    var option = $(this);
                    var optionId = $(this).val();
                    $(lookupData.ListingCategory).each(function() {
                        if (this.Id == optionId) {                          
                            if (this.OnlineName != $(item).getCell(rowid, 'OnlineName')) {
                                option.remove();
                                return false;
                            }
                        }
                    });
                });
            },

            search: true,
            searchdata: {},
            caption: "List of all Traffic lines",
            editurl: editURL + '?rows=' + $rows + '&page=1',
            hiddengrid: hideGrid


Here is the JSON data returned via the Ajax call during the jqGrid function call above (NOTE it must be { async: false}:
{"ListingCategory":[{"Id":29,"Name":"Document Imaging","OnlineName":"RF Globalnet"}   
,{"Id":1,"Name":"Ancillary Department","OnlineName":"Healthcare Technology Online"} 
,{"Id":2,"Name":"Asset Tracking","OnlineName":"Healthcare Technology Online"}   
,{"Id":3,"Name":"Asset Tracking","OnlineName":"RF Globalnet"}   
,{"Id":4,"Name":"Asset Tracking","OnlineName":"ISMR"}   
,{"Id":5,"Name":"Document Imaging","OnlineName":"Healthcare Technology Online"} 
,{"Id":6,"Name":"Document Imaging","OnlineName":"RF Globalnet"}   
,{"Id":7,"Name":"EMR/EHR Software","OnlineName":"Healthcare Technology Online"}]}
I only need the Id and Name for the drop down list, but the third column in the JSON object is important, it is the only that I match up with the OnlineName in the jqGrid column, and then in the loop during afterEditCell simply remove the ones I don't want the user to see. That's it!

Thursday, December 30, 2010

Upgrade from Asp.Net MVC 1 to MVC 2 - how to and issues with JsonRequestBehavior

Goal

Upgrade your MVC 1 app to MVC 2

Issues

You may get errors about your Json data being returned via a GET request violating security principles - we also address this here. This post is not intended to delve into why the Json GET request is or may be an issue, just how to resolve it as part of upgrading from MVC1 to 2.

Solution

First remove all references from your projects to the MVC 1 dll and replace it with the MVC 2 dll. Now update your web.config file in your web app root folder by simply changing references to assembly="System.Web.Mvc, Version 1.0.0.0 to Version 2.0.0.0, there are a couple of references in your config file, here are probably most of them you may have:
        <compilation debug="true" defaultLanguage="c#">
           
        masterPageFile="~/Views/Masters/CRMTemplate.master" pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"
            <controls>
                System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" namespace="System.Web.Mvc" tagPrefix="mvc" />
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" validateRequest="False">

Secondly, if you return Json objects from an ajax call via the GET method you ahve several options to fix this depending on your situation:
1. The simplest, as in my case I did this for an internal web app, you may simply do:
            return Json(myObject, JsonRequestBehavior.AllowGet);

2. In Mvc if you have a controller base you could wrap the Json method with:
        public new JsonResult Json(object data)
        {
            return Json(data, "application/json", JsonRequestBehavior.AllowGet);           
        }

3. The most work would be to decorate your Actions with:
        [AcceptVerbs(HttpVerbs.Get)]

4. Another tnat is also a lot of work that needs to be done to every ajax call returning Json is:
                            msg = $.ajax({ url: $('#ajaxGetSampleUrl').val(), dataType: 'json', type: 'POST', async: false, data: { name: theClass }, success: function(data, result) { if (!result) alert('Failure to retrieve the Sample Data.'); } }).responseText;

This should cover all the issues you may run into when upgrading. Let me kow if you run into any other ones.

jQuery 1.4.4 - issue with attr('selected', null)

Issue:
The code below worked before under version jQuery 1.4.2 but when I upgraded to version 1.4.4 it no longer worked as expected - it did not unselect the list box item, only setting "selectd" worked:
        _handleClick: function(elem) {
            var self = this; var initElem = this.element;
            var checked = $(elem).attr('checked');
            var myId = elem.attr('id').replace(initElem.attr('id') + '_chk_', '');
            initElem.children('option[value=' + myId + ']').attr('selected', function() {
                if (checked) {
                    return 'selected';
                } else { return null; }
            });

            if ($.isFunction(self.options.onItemSelected)) {
                try {
                    self.options.onItemSelected(elem, initElem.children('option').get());
                } catch (ex) {
                    if (self.options.allowDebug)
                        alert('select function failed: ' + ex.Description);
                }
            }
        },
Solution:
Under jQuery 1.4.4 you need to explicitly remove the attribute as in "removeAttr('selected'):
        _handleClick: function(elem) {
            var self = this; var initElem = this.element;
            var checked = $(elem).is(':checked');
            var myId = elem.attr('id').replace(initElem.attr('id') + '_chk_', '');
            if (checked) {
                initElem.children('option[value=' + myId + ']').attr('selected', 'selected');
            } else {
                initElem.children('option[value=' + myId + ']').removeAttr('selected');
            }
            if ($.isFunction(self.options.onItemSelected)) {
                try {
                    self.options.onItemSelected(elem, initElem.children('option').get());
                } catch (ex) {
                    if (self.options.allowDebug)
                        alert('select function failed: ' + ex.Description);
                }
            }
        },

Tuesday, December 14, 2010

After restoring a SQL Server database from another server - get login fails

Issue:

After you have restored a sql server database from another server, lets say from production to a Q/A environment, you get the "Login Fails" message for your service account.

Reason:

User logon information is stored in the syslogins table in the master database. By changing servers, or by altering this information by rebuilding or restoring an old version of the master database, the information may be different from when the user database dump was created. If logons do not exist for the users, they will receive an error indicating "Login failed" while attempting to log on to the server. If the user logons do exist, but the SUID values (for 6.x) or SID values (for 7.0) in master..syslogins and the sysusers table in the user database differ, the users may have different permissions than expected in the user database.

Solution:

Links a user entry in the sys.database_principals system catalog view in the current database to a SQL Server login of the same name. If a login with the same name does not exist, one will be created. Examine the result from the Auto_Fix statement to confirm that the correct link is in fact made. Avoid using Auto_Fix in security-sensitive situations.

When you use Auto_Fix, you must specify user and password if the login does not already exist, otherwise you must specify user but password will be ignored. login must be NULL. user must be a valid user in the current database. The login cannot have another user mapped to it.
execute the following stored procedure, in this example the login user name is "MyUser"
exec sp_change_users_login 'Auto_Fix', 'MyUser'

NOTE:
sp_change_users_login cannot be used with a SQL Server login created from a Windows principal or with a user created by using CREATE USER WITHOUT LOGIN.After restoring a SQL Server database from another server - get login fails

Thursday, December 2, 2010

Maximum request length exceeded.

Issue:
The file upload size on your machine or web app is too small. The default file upload size for IIS is 4096K. The file size the user is trying to add can be retrieved (using the ELMAH log here) by looking at the Content-Length header property:

HTTP_CONNECTION:keep-alive HTTP_KEEP_ALIVE:115 HTTP_CONTENT_LENGTH:6414851

In this example it is just over 6Mb

Solution:
You can add/update the HttpRuntime property in the machine or web config files, depending on at what level you want to set the size. For web-server level, impacting all web-apps running on that machine specify the following in the node of the machine.config file to allow less than 1Mb:

      <system.web>
<httpRuntime maxRequestLength="1000" />

If you want to set it, bigger, smaller or block at the web-app level you can specify it by adding the property above to the web.config file of that web app and it will only affect that web app.

For example I would like to maintain the default 4096K size limitation on my server except for the accounting web app and therefore set the accounting web-sites virtual directory web-config file to handle 8Mb file uploads:

      <system.web>
<httpRuntime maxRequestLength="8192" />