Friday, February 5, 2010

GMail Style Multiple Checkbox Shift Selection

A while ago I ran into a situation where there were a list of check boxes in a ASP.NET GridView control and I wanted to be able to Shift+Click the check boxes like GMail or make a selection in Windows Explorer.

Here is the mocked up code that I came up with using jQuery. I make extensive use of is the predicate selector '$='. Which a very useful trick to get around injecting the ASP.NET client id into JavaScript.
var selectedCheckRow = 2;
function itemToggleSelect(cb, e) {
    var checkAll = $("input[id$='CheckAll']"),
        chkVal = cb.checked,
        idVal = cb.id;
    if (!chkVal && checkAll.attr('checked')) {
        $(".selectAllMsg").hide();
        selectAllPages('false');
        checkAll.attr('checked', false);
    }
    // -- Shift-Click Select on Item Grid -- //
    if (!e) e = event;
    CurrentCheckRow = $("#" + idVal)[0].parentNode.parentNode.rowIndex;
    if (e.shiftKey) {
        ((CurrentCheckRow > selectedCheckRow) ?
            $("input[id$='chkAction']").slice(selectedCheckRow - 2, CurrentCheckRow - 2)    // Selecting down
            : $("input[id$='chkAction']").slice(CurrentCheckRow - 2, selectedCheckRow - 2)  // Selecting up
        ).attr('checked', chkVal)
    }
    // Save checkbox row
    selectedCheckRow = $("#" + idVal)[0].parentNode.parentNode.rowIndex;
}
//Expects: 'true' if all pages should be selected
//         'false' if select only viewable items/page.
function selectAllPages(allPages) {
    $(".selectAllPages").val(allPages);
    var selectAll = $(".selectAllMsg > td");
    selectAll.html((allPages == "true") ?
        "All items on all pages have been selected.  <a  href='#' onclick='ToggleAll(false);return false;'>Clear Selection.</a>" :
        "All items on this page are selected.  <a href='#' onclick='selectAllPages(true);return false;'>Select All Items on All Pages.</a>");
}
function ToggleAll(cb) {
    var msg = $(".selectAllMsg"), selectAll,
        chkVal = cb.checked,
        idVal = cb.id;
    if (chkVal) {
        selectAll = $(".selectAllMsg > td");
        selectAll.html("All items on this page are selected.  <a href='#' onclick='selectAllPages(true);return false;'>Select All Items on All Pages.</a>");
        msg.slideDown(0);
    }
    else
    {
        msg.hide();
        selectAllPages('false');
    }
    $("input[id$='chkAction'],input[id$='CheckAll']").attr('checked', chkVal);
}

Demo
The demo code (view the source to see) is setup to mimic an GridView control. To make this demo function you can click a check box, perhaps '3' and then shift+click another check box, perhaps '7'. Not overly complicated, but could definitely save time over developing from scratch.





Select All




1


2


3


4


5


6


7


8


9


10


11


12


13


14


15


16


17

Sunday, January 31, 2010

Enumeration of All Users in a Group (traversing nested groups) in Active Directory

As a counter part to my previous post, I created a dual of the script which enumerates Groups and their members.

#
#    Enumerate All Users for a Group (including traversing nested groups) 
#        @param distinguishedName - The distinguished name of the object you want to traverse
#    Brock Moeller
#    12/16/2009
#
param (
    $distinguishedName = "CN=SuperGroup,OU=Groups,DC=serv,DC=ubercorp,DC=com" 
)

$roles = @{};
$indent = -1;
filter EnumMember {
    $indent += 1;
    if ($_ -is [System.DirectoryServices.DirectoryEntry]){
        $adsiObj = $_;
    } else {
        $adsiObj = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + $_);
    }
    if ((-not [String]::IsNullOrEmpty($adsiObj.cn)) -and (-not $roles.ContainsKey($adsiObj.cn))){
        $roles[$adsiObj.cn] = 1;
        $memberOfCount = $adsiObj.member.Count;
        $("`t"*$indent) + $adsiObj.cn + " [$memberOfCount]";
        if (($memberOfCount -gt 0) -and ($indent -lt 900)) {
            $adsiObj.member | EnumRoles;
        }
    }
    $indent -= 1;
}

$user = [adsi]"LDAP://$distinguishedName";
$user;
$buffer = $user.Path + "`n";
$user | EnumMember | % { $buffer += $_ + "`n" };
"Buffer: " + $buffer.ToString();
[System.IO.File]::WriteAllText("$pwd\$($user.cn).txt", $buffer.ToString());

Friday, January 29, 2010

Enumeration of All Groups for a User (traversing nested groups) in Active Directory

I ran into an odd situation where I needed to be able to enumerate all of the groups that a user was a member. I found a lot of programs and scripts that will list the groups for a user, but none that would traverse nested groups.

The script writes the information to a file for better reviewing later. The output adds indentation to help readability.

Here is my solution.

#
#    Enumerate All Groups for a User (including traversing nested groups) 
#        @param distinguishedName - The distinguished name of the object you want to traverse
#        Brock Moeller
#        12/16/2009
#
param (
    $distinguishedName = "CN=Joe Dirt,OU=Users,DC=serv,DC=ubercorp,DC=com" 
)

$roles = @{};
$indent = -1;
filter EnumRoles {
    $indent += 1;
    if ($_ -is [System.DirectoryServices.DirectoryEntry]){
        $adsiObj = $_;
    } else {
        $adsiObj = New-Object System.DirectoryServices.DirectoryEntry("LDAP://" + $_);
    }
    if ((-not [String]::IsNullOrEmpty($adsiObj.cn)) -and (-not $roles.ContainsKey($adsiObj.cn))){
        $roles[$adsiObj.cn] = 1;
        $memberOfCount = $adsiObj.memberOf.Count;
        $("`t"*$indent) + $adsiObj.cn + " [$memberOfCount]";
        if (($memberOfCount -gt 0) -and ($indent -lt 900)) {
            $adsiObj.memberOf | EnumRoles;
        }
    }
    $indent -= 1;
}

$user = [adsi]"LDAP://$distinguishedName";
$user;
$buffer = $user.Path + "`n";
$user | EnumRoles | % { $buffer += $_ + "`n" };
"Buffer: " + $buffer.ToString();
[System.IO.File]::WriteAllText("$pwd\$($user.cn).txt", $buffer.ToString());

Tuesday, January 26, 2010

Google Reader Grease Monkey Script

I was bored one day and decided that I wanted to fix up the Google Reader interface a little bit and this was the result. Sure there are more useful GM scripts, but this is a start.
// ==UserScript==
// @name           Google Reader Fixup
// @namespace      http://intellectualponderings.blogspot.com
// @version        1.0
// @description    Hides the sliding navigation pane.  Hides header below the blog name and the footer bar.  Adds "Info" button next to the blog name to toggle header and footer bar.  
// @include        htt*://www.google.*/reader*
// @include        http://www.google.com/reader/view/*
// ==/UserScript==

var cssHidechromeheader = <><![CDATA[
    #viewer-header, #viewer-footer { display: none; }
    ]]></>.toString();
var cssShowchromeheader = <><![CDATA[
    #viewer-header, #viewer-footer { display: inline; }
    ]]></>.toString();
var cssHideNav= <><![CDATA[
    #chrome-lhn-toggle { display: none; }
    ]]></>.toString();
 
function getElementPosition(element) {
    var pos = {x:0, y:0};
    if (element.offsetParent) {
        while (element.offsetParent) {
            pos.x += element.offsetLeft;
            pos.y += element.offsetTop;
            element = element.offsetParent;
        }
    } else if (element.x) {
        pos.x += element.x;
        pos.y += element.y;
    }
    return pos; 
}

function InfoMouseClick(e) {
    var gei = document.getElementById,
        header = gei('viewer-header'),
        hDisplay = header.style.display,
        footer = gei('viewer-footer'),
        entries = gei('entries'),
        entriesStatus = gei('entries-status');
    if ((hDisplay == "none")||(hDisplay == null)||(hDisplay == "")) {
        header.style.display = footer.style.display = "inline";
        entriesStatus.style.right = "0.5em";
    } else {
        header.style.display = footer.style.display = "none";
    }
    entries.style.height = String(d.documentElement.clientHeight - getElementPosition(entries).y - footer.offsetHeight) + "px";
}

(function () {
    //Initial Styles
    GM_addStyle( cssHidechromeheader );
    GM_addStyle( cssHideNav );
    var chrome = document.getElementById('chrome-header');
    //Button Element
    var buttonStr = " 
 
Info
"; var googbutton=document.createElement('div'); googbutton.className='goog-button-base goog-button-base-outer-box goog-inline-block'; googbutton.style.margin = "0px 0px 0px 6px"; googbutton.innerHTML=buttonStr; chrome.appendChild(googbutton); googbutton.addEventListener('click', InfoMouseClick, false); })();

Friday, November 13, 2009

SharePoint 2007 Forms Authentication Error "Value cannot be null."

Every so often, users logging into a SharePoint 2007 portal via forms authentication with a custom membership provider get the following error message. Once the message starts to be received, it will continue to function in that manner.

Value cannot be null.
Parameter name: value at System.String.EndsWith(String value, StringComparison comparisonType)
at Microsoft.SharePoint.ApplicationRuntime.SPRequestModule.PostAuthenticateRequestHandler(Object oSender, EventArgs ea)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

The error was not resulting from any of my code which made this incredibly hard to figure out. If you restart the IIS and the OWSTimer, the issue goes away (for a while), but doesn't fix this issue.

This morning I was searching around with a very odd set keywords and ran across this article, Fixing the Elusive “Value Cannot Be Null” FBA Authentication Error. The page doesn't contain the error text or a specific solution, but it does contain a screen shot of exactly what I was experiencing.

I have included the text and solution in this blog to make it crawl-able.

The solution:
The issue appears to be the fact that the web.config, C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\TEMPLATE\LAYOUTS\web.config, in the layouts directory of the hive has batch compilation set to false.
<compilation batch="false" batchTimeout="600" maxBatchSize="10000" maxBatchGeneratedFileSize="10000" />

The line should read:

<compilation batch="true" batchTimeout="600" maxBatchSize="10000" maxBatchGeneratedFileSize="10000" />

After making this change, the performance of the site in general improved significantly on the Windows and Forms authentication sides. It looks like an all-around win.


Batch Compilation Information:
From Microsoft's KB article, How to use the "batch" attribute of the Web.config file compilation element in SharePoint Server 2007 and in Windows SharePoint Services 3.0:
The batch attribute is used by the ASP.NET compilation element. This attribute controls all compilation for applications to which the Web.config file applies.

When the attribute is set to "true," the delay that you experience when you access files for the first time can be decreased. This is because, when the batch attribute is set to "true," all uncompiled files will be compiled in batch mode by ASP.NET.

However, for larger applications, there may be a significant delay when files are compiled for the first time because there are more file batches to compile. After the initial compilation, delays are decreased when you access the compiled files.

Microsoft has an excellent web performance best practices site, Developing High-Performance ASP.NET Applications. The pertinent part for our issue:
Consider precompiling
A Web application is batch-compiled on the first request for a resource such as an ASP.NET Web page. If no page in the application has been compiled, batch compilation compiles all pages in a directory in chunks to improve disk and memory usage. You can use the ASP.NET Compilation Tool (Aspnet_compiler.exe) to precompile a Web application. For in-place compilation, the compilation tool calls the ASP.NET runtime to compile the site in the same manner as when a user requests a page from the Web site. You can precompile a Web application so that the UI markup is preserved, or precompile the pages so that source code cannot be changed. For more information, see How to: Precompile ASP.NET Web Sites.

This article explains in depth how debug and batch compilation works. It is worth the read. ASP.NET Resources - Beware Of Deploying Debug Code In Production

Thursday, August 27, 2009

XQuery Rename/Replace Xml Node and Update K2 Workflow Process Instance Xml Fields

I recently ran across a case where a property in our application was re-termed due to a change in requirements. Updating the code was very easy, however it meant that all of the Xml documents needed to be updated and that was not going to be as easy. It would involve updating the serialized object xml documents in our workflow data repository and K2.

Out of curiosity I ran the process using the old Xml to see what would happen. Deserializing the old Xml documents resulted in everything after that renamed property being omitted from the deserialization and it didn't throw and error. That was unexpected, but good information to know.

As it turns out, updating the workflow data repository was easy. K2 has many issues which resulted in the K2 update sql. The code below shows how I accomplished this:
CREATE PROCEDURE [dbo].[usp_brock_RenameXmlNode]
AS
BEGIN
 -- UPDATE Workflow Instances
 UPDATE [WorkflowInstances] SET InstanceData.modify('
  insert element NewTag {/ObjectXML/OldTag/text()} after (/ObjectXML/OldTag)[1]
 ')
 WHERE  WorkflowID='7B4DEB62-D462-4FE8-A8E2-2057B5F31B19'
   AND InstanceData.exist('/ObjectXML/OldTag') = 1
 UPDATE [WorkflowInstances] SET InstanceData.modify('
  delete (//OldTag)[1]
 ')
 WHERE  WorkflowID='7B4DEB62-D462-4FE8-A8E2-2057B5F31B19'
   AND InstanceData.exist('/ObjectXML/OldTag') = 1
 
 -- UPDATE K2 Workflows
 -- Create Table Var and Populate
 DECLARE @K2ProcXml2XmlType table
 (
  ProcInstID INT PRIMARY KEY,
  WorkflowName VARCHAR(128),
  WorkflowID UNIQUEIDENTIFIER,
  InstanceData XML,
  IsModified BIT
 )
 PRINT 'POPULATE TABLE VAR';
 INSERT INTO @K2ProcXml2XmlType 
  SELECT pin.ID
     ,ps.[Name] WorkflowName
     , wi.WorkflowID
     ,CONVERT(XML,REPLACE(CAST(f.string as nvarchar(max)),'<?xml version="1.0" encoding="utf-8"?>','')) InstanceData
     , 0 --IsModified
    FROM [K2].[dbo]._FieldOnDemand f INNER JOIN [K2].[dbo]._ProcInst pin ON f.ProcInstID=pin.ID
    INNER JOIN [DemoDatabase].dbo.WorkflowInstances wi ON wi.ProcessID=pin.ID
    INNER JOIN [K2].[dbo]._Proc p ON pin.ProcID = p.ID
    INNER JOIN [K2].[dbo]._ProcSet ps ON p.ProcSetID = ps.ID
  WHERE  (ps.[Name] in ('WorkflowX'))

 UPDATE @K2ProcXml2XmlType
 SET InstanceData.modify('insert element NewTag {/ObjectXML/OldTag/text()} after (/ObjectXML/OldTag)[1]'), IsModified = 1
 WHERE (InstanceData IS NOT NULL) 
  AND (InstanceData.exist('/ObjectXML/OldTag') = 1)

 UPDATE @K2ProcXml2XmlType
 SET InstanceData.modify('delete (//OldTag)[1]'), IsModified = 1
 WHERE (InstanceData IS NOT NULL) 
  AND (InstanceData.exist('/ObjectXML/OldTag') = 1)


 -- End Result of all updates
 --PRINT 'QUERY RESULT:';
 --SELECT
 --        [ProcInstID]
 --   , WorkflowName
 --   , InstanceData
 --  FROM @K2ProcXml2XmlType src
 --WHERE src.IsModified = 1;

 -- Populate Original Table and Query
 PRINT 'POPULATE TABLE FROM VAR:';
 UPDATE dst
 SET dst.String = CONVERT(ntext,'<?xml version="1.0" encoding="utf-8"?>'+CAST(src.InstanceData as nvarchar(max)))
 FROM @K2ProcXml2XmlType src INNER JOIN [K2].[dbo].[_FieldOnDemand] dst ON src.ProcInstID = dst.ProcInstID
 WHERE src.IsModified = 1;

        -- END K2 Update
END;
GO

exec dbo.usp_brock_RenameXmlNode

DROP PROCEDURE dbo.usp_brock_RenameXmlNode
It runs quite fast.

There are a couple caveats, you can't create the XPath location dynamically (unless you are using dynamic sql which I try to avoid at all costs). This means that you can't pass old and new name parameters into the stored procedure and update the records. Disappointing as it is, any future renames should require an easy find and replace.

I tried using a cursor to do this type of updating, but the resulting SQL in execution times in more than more than 90 minutes (I cancelled it at that point) and verified with the DBA's that I was not doing something wrong, I abandoned that path and went back to the set based updates.