Archives

Archives / 2013 / February
  • Defaulting Values in a Multi-Lookup Form in SharePoint

    This was a question asked on the MSDN Forums but I thought it was worthy of a blog post as I could get more in depth with the explanation and show some pretty pictures (plus the fact I’ve never done it so thought it would be fun).

    The problem was a user wanted to default multiple values in a lookup field in SharePoint. First problem, there are no defaults in a lookup field. Second problem, how do you do default multiple values?

    First we’ll start with the setup. Create yourself a list which will hold the lookup values. In this case it’s a list of country names but it can be anything you want. Just a custom list with the Title field is enough.

    image

    Now we need a list with a lookup column to select our countries from. Create another custom list and add a column to it that looks something like this. Here’s the name and type:

    image

    And here’s the additional column settings where we get our information from (MultiLookupDefaultSpikeSource is the name of the list we created to hold our values)

    image

    Here’s what our form looks like when we add a new item:

    image

    Thinking about the problem I first though we could manipulate the form in SharePoint Designer but realized that the Form Web Part is going to retrieve all of our values from the list, defaults, etc. and really what we need to do is manipulate the list at runtime in the DOM.

    It’s jQuery to the RESCUE!

    First we take a look at the original state of the form to find our list boxes. Here’s the snippet we’re interested in, the first listbox:

     <select   
     name="ctl00$m$g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f$ctl00$ctl05$ctl01$ctl00$ctl00$ctl04$ctl00$ctl00$SelectCandidate"   
     title="Country possible values"   
     id="ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_SelectCandidate"   
     style="width: 143px; height: 125px; overflow: scroll;"   
     ondblclick="GipAddSelectedItems(ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false"   
     onchange="GipSelectCandidateItems(ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m);"   
     multiple="multiple">   
     <OPTION title=Africa selected value=5>Africa</OPTION>   
     <OPTION title=Asia value=1>Asia</OPTION>   
     <OPTION title=Europe value=3>Europe</OPTION>   
     <OPTION title=India value=4>India</OPTION>   
     <OPTION title=Ireland value=6>Ireland</OPTION>   
     <OPTION title=Singapore value=2>Singapore</OPTION>   
     </select>  
    

    We can see that it has an ID that ends in “_SelectCandidate” so we’ll use this for selection.

    Another part of the puzzle is a hidden set of fields that store the actual values used in the list. There are three of them and they’re well documented in a blog post here by Marc Anderson on SharePoint Magazine. In it he talks about multiselect columns and breaks down the three hidden fields used (the current set of values, the complete set of values, and the default values).

    The second listbox looks like this:

     <select   
     name="ctl00$m$g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f$ctl00$ctl05$ctl01$ctl00$ctl00$ctl04$ctl00$ctl00$SelectResult"   
     title="Country selected values"   
     id="ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_SelectResult"   
     style="width: 143px; height: 125px; overflow: scroll;"   
     ondblclick="GipRemoveSelectedItems(ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m); return false" \   
     onchange="GipSelectResultItems(ctl00_m_g_478fe6d2_8fdb_48e8_be57_7739de1c3b8f_ctl00_ctl05_ctl01_ctl00_ctl00_ctl04_ctl00_ctl00_MultiLookupPicker_m);"   
     multiple="multiple">  
    

    Easy enough. It has an ID that contains “_SelectResult”.

    Now a quick jQuery primer when selecting items:

    • $("[id='foo']"); // id equals 'foo'
    • $("[id!='foo']") // id does not equal 'foo'
    • $("[id^='foo']") // id starts with 'foo'
    • $("[id$='foo']") // id ends with 'foo'
    • $("[id*='foo']") // id contains 'foo'

    Simple. We want to find the control that ends with “_SelectCandidate” and remove some items, then find the control that ends with “_SelectResult” and append our selected items.

    So a few lines of heavily commented JavaScript:

     $(document).ready(function(){  
       // define the items to add to the results (i.e already selected) this the visual part only   
       var $resultOptions = "<OPTION title=Africa value=5>Africa</OPTION><OPTION title=India value=4>India</OPTION><OPTION title=Ireland value=6>Ireland</OPTION>";   
       // this is the list of initial items (matching the ones above) that are used when the item is saved   
       var $resultSpOptions = "5|tAfrica|t4|tIndia|t6|tIreland";   
       // find the possible values control   
       var possibleValues = $("[id$='_SelectCandidate']");  
       // remove 1st option (Africa)   
       $("[id$='_SelectCandidate'] option:eq(0)").remove();  
       // remove 3rd option (India)   
       $("[id$='_SelectCandidate'] option:eq(2)").remove();  
       // remove 3rd option (Ireland)   
       $("[id$='_SelectCandidate'] option:eq(2)").remove();  
       // set selected value to asia (value 1)   
       possibleValues.val(1)  
       // append the new options to our results (this updates the display only of the second list box)   
       $("[id$='_SelectResult']").append($resultOptions);  
       // append the new options to our hidden field (this sets the values into the list item when saving)   
       $("[id$='MultiLookupPicker']").val($resultSpOptions);   
     });  
    

    SharePoint 2010 supports editing NewForm.aspx (and the other out-of-the-box forms) in the browser. One option is to modify the list and under advanced settings you can disable “Launch forms in a dialog”. This will launch the form like a regular web page. However that’s 3 or 4 steps and you have to go back and change it when you’re done.

    Instead just visit the new form directly:

    http://sitename/listname/NewForm.aspx

    From this page select Site Actions | Edit Page. Now you can add a Content Editor Web Part to the page. When adding JavaScript I point the Content Link to the .js file (that I upload somewhere like Style Library or the Assets library if you have one) rather than trying to put JavaScript into the Content Editor Web Part. This way a) I can edit the JavaScript outside of the page by loading it up in SharePoint Designer or even upload a new .js file to the library and b) I can debug the JavaScript independently of the NewForm.aspx page (or whatever page I’m adding the .js file to)

    The result:

    image

    When you save the record, the three default options are saved as well (this was set by the JavaScript).

    Hope that helps!

  • Where tips in LINQ

    These might be old but as I was going through doing some code reviews and optimizations I thought I would share with the rest of the class.

    Count() > 0 vs. Any

    This is a bit of heated debate but as you dive in the LINQ world you'll start seeing simpler ways to write things. LINQ itself gets you away from writing loops for example (sometimes). One thing I notice in code are things like this:

    if(entity.Where(some condition).Count() > 0)

    When you could just write this instead:

    if(entity.Any(some condition))

    For me, writing *less* code is *more* important. It's a cornerstone in refactoring. Making your code more readable, more succinct. If you can scan code quicker while troubleshooting a problem or trying to figure out where to add an enhancement, all the better.

    Donn Felker wrote  about the Count() vs. Any() discussion here back in 2009 and it was discussed on StackOverflow here in case you're wondering if Any() is more performant than Count() (it generally is). So my advice is use Any() to make your code potentially easier to read unless there's a performance problem.

    Another simple tip for code reduction is this one.

    var x = entity.Where(some condition).FirstOrDefault();

    Becomes:

    var x = entity.FirstOrDefault(some condition);

    I know it's simple but again it's about readability. The better you can scan someone elses new code you've never seen before is less time stumbling over the syntax and more about what the intention is.

    Yes, these tips are old but you would be surprised seeing new developers write new code with them and wonder why?

  • Windows 8 and the Lethbridge Technology User Group

    Join me and 83,517 screaming nerds (everyone in the city is attending and a geek right?) on Thursday February 21st from 3-5pm to talk about building Metro style apps for WIndows 8. Here's what we'll be covering.

    Windows 8 Platform for Metro Style Apps

    Windows 8 is Windows re-imagined. Join this session to learn about the new platform for building Metro style apps. Get an understanding of the platform design tenets, the programming language choices and the integration points with the operating system and across Metro style apps

    Everything Web Developers Must know to build Metro Style Apps

    Learn how you can use your web skills to build Windows 8 Metro style apps using HTML5, CSS3 and JavaScript. In this session you’ll discover how to harness the rich capabilities of Windows 8 through JavaScript and the Windows Runtime. You will learn about navigation, user experience patterns and controls, inherent async design and the seamless integration with the operating system that will let you create great Metro style apps.

    I promise fun times, chaos monkeys, and live kitten juggling as per my usual presentations.

  • PowerShell Tools - Removing Orphaned Users from SharePoint

    Here’s a script that will walk through all Site Collections in all Web Applications (i.e. your entire farm) and delete any user from the Site Collection that isn’t in ActiveDirectory anymore.

    Note this will not remove them from their user profiles, it just cleans up Site Collections. If you want some great info on how the User Profile service works and the My Site Cleanup Job then check out these resources:

    Here’s the code:

    [int]$GLOBAL:TotalUsersUpdated = 0;
     
    function Check_User_In_ActiveDirectory([string]$LoginName, [string]$domaincnx)
    {
        $returnValue = $false
        $strFilter = "(&(|(objectCategory=user)(objectCategory=group))(samAccountName=$LoginName))"
        $objDomain = New-Object System.DirectoryServices.DirectoryEntry($domaincnx)
     
        $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
        $objSearcher.SearchRoot = $objDomain
        $objSearcher.PageSize = 1000
        $objSearcher.Filter = $strFilter
        $objSearcher.SearchScope = "Subtree"
     
        $colResults = $objSearcher.FindAll()
     
        if($colResults.Count -gt 0)
        {
            $returnValue = $true
        }
     
        return $returnValue
    }
     
    function ListOrphanedUsers([string]$SiteCollectionURL, [string]$mydomaincnx)
    {
        [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") > $null
        $site = new-object Microsoft.SharePoint.SPSite($SiteCollectionURL)
        $web = $site.openweb()
     
        Write-Host "SiteCollectionURL:", $SiteCollectionURL
     
        $siteCollUsers = $web.SiteUsers
        Write-host "SiteUsers:", $siteCollUsers.Count
        
        #Create array to hold non-existant users
        $usersToRemove = @()
     
        foreach($MyUser in $siteCollUsers)
        {
            if(($MyUser.LoginName.ToLower() -ne "sharepoint\system") -and 
                ($MyUser.LoginName.ToLower() -ne "nt authority\authenticated users") -and 
                ($MyUser.LoginName.ToLower() -ne "nt authority\local service"))
            {
                $UserName = $MyUser.LoginName.ToLower()
                $Tablename = $UserName.split("\")
                
                $returncheck = Check_User_In_ActiveDirectory $Tablename[1] $mydomaincnx 
                if($returncheck -eq $False)
                {
                    Write-Host "User does not exist", $MyUser.LoginName, "on domain"
                    $usersToRemove = $usersToRemove + $MyUser.LoginName
                    $GLOBAL:TotalUsersUpdated += 1;
                }
            }
        }
        
        foreach($u in $usersToRemove)
        {
            Write-Host "Removing", $u, "from site collection", $SiteCollectionURL
            $siteCollUsers.Remove($u)
        }
     
        $web.Dispose()
        $site.Dispose()
    }
     
    function ListOrphanedUsersForAllColl([string]$WebAppURL, [string]$DomainCNX)
    {
        [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") > $null
     
        $Thesite = new-object Microsoft.SharePoint.SPSite($WebAppURL)
        $oApp = $Thesite.WebApplication
        Write-host "Total Site Collections:", $oApp.Sites.Count
     
        $i = 0
        foreach ($Sites in $oApp.Sites)
        {
            $i = $i + 1
            Write-Host "---------------------------------------"
            Write-host "Collection Number", $i, "of", $oApp.Sites.Count
     
            if($i -gt 0)
            {
                $mySubweb = $Sites.RootWeb
                $TempRelativeURL = $mySubweb.Url
                ListOrphanedUsers $TempRelativeURL $DomainCNX
            }
        }
     
        Write-Host "======================================="
     
    }
     
    function EnumerateAllSiteColl()
    {
        $farm = Get-SPWebApplication | select DisplayName
        foreach($app in $farm)
        {
            $webapp = Get-SPWebApplication | ? {$_.DisplayName -eq $app.DisplayName}
            Write-Host "Web Application:", $webapp.DisplayName
            ListOrphanedUsersForAllColl $webapp.Url "LDAP://DC=ca,DC=util"
            Write-Host
        }
    }
     
    function StartProcess()
    {
        cls
     
        [System.Diagnostics.Stopwatch] $sw;
        $sw = New-Object System.Diagnostics.StopWatch
        $sw.Start()
     
        EnumerateAllSiteColl
     
        $sw.Stop()
     
        write-host "***************************"
        write-host $GLOBAL:TotalUsersUpdated, "users removed in", $sw.Elapsed.ToString()
        write-host "***************************"
    }
     
    StartProcess


    Enjoy!