Feeds:
Posts
Comments

Archive for the ‘SP2010 Development’ Category

As SharePoint Developers, we are often challenged with requests to sort list item data seemingly arbitrarily. In these cases, none of the field data can be used to sort ascending or descending and get the results we are seeking. Often, we end up creating a Number column and setting a value so we can sort on that value. The trouble here is that you must manually update all of the other items. This can be painful if you’re trying to move an item with Order = 50 to Order = 1: You must change the Order value on 49 other items!

Links Lists have a handy link in the Items menu labelled “Change Item Order.” The page this link takes you to allows you to change the order of items in the Links List just like you would change the order of fields in a list view. SharePoint then stores the numerical sort order in a hidden field on the list which the Links Lists view uses with an orderBy.

So, excellent, there is something out of box to do what we want to do. Hooray! Now, how do we make use of it? Turns out, if you add a column to your custom list that is identical to the hidden column used in the Links List, you can take advantage of the “Change Item Order” page for your custom list! Here’s how I did it:

Using SharePoint REST API, I discovered the SchemaXml property for the hidden Order field in the Links List is:

<Field ID=\"{ca4addac-796f-4b23-b093-d2a3f65c0774}\" ColName=\"tp_ItemOrder\" RowOrdinal=\"0\" Name=\"Order\" DisplayName=\"Order\" Type=\"Number\" Hidden=\"TRUE\" SourceID=\"http://schemas.microsoft.com/sharepoint/v3\" StaticName=\"Order\" FromBaseType=\"TRUE\" />

I then simply generated a POST using the REST API once again to create a field on my custom list using the SchemaXml above. You can, of course, use whatever app you wish to do the POST. My favorite is Postman. Here’s how I setup the request to send:

  1. POST to [your site URL]/_api/web/lists/getbytitle(‘[title of your custom list]‘)/fields/createfieldasxml
  2. Headers:
    1. Accept: application/json
    2. Content-Type: application/json
    3. X-RequestDigest: [a current request digest]
  3. Body:
    {
        "parameters": {
           "__metadata": {
              "type": "SP.XmlSchemaFieldCreationInformation"
           },
           "SchemaXml": "&lt;Field ID=\"{ca4addac-796f-4b23-b093-d2a3f65c0774}\" ColName=\"tp_ItemOrder\" RowOrdinal=\"0\" Name=\"Order\" DisplayName=\"Order\" Type=\"Number\" Hidden=\"TRUE\" SourceID=\"http://schemas.microsoft.com/sharepoint/v3\" StaticName=\"Order\" FromBaseType=\"TRUE\" /&gt;"
        }
    }

This adds a hidden Order field to your custom list. The only thing left to do is access the “Change Item Order” page for your list and start setting item order. Here’s the URL:

[your site URL]/_layouts/15/Reorder.aspx?List=[your list GUID]

If you want to get the ordering data for your client-side app, here’s an important tip: The SharePoint REST API does not return the Order field when you simply execute a call to _api/web/Lists…/items. If you add Order to a $select option—like _api/web/Lists…/items?$select=Order—the item ordering set by the “Change Item Order” page will be returned.

Happy ordering! Let me know below if you find any issues doing this.

Read Full Post »

Steve Peschka put together a great blog post about Bypassing the Multi Authentication Provider Selection Page in SharePoint 2010 that was also incorporated in a Microsoft MSDN article. (If you’re not concerned with SharePoint 2010 and are here for info regarding SharePoint 2013, skip down to the “SharePoint 2013” heading.) The workaround Peschka provides works, but I had a few issues with how it was put together:

  1. It’s not a solution deployment. Manual. 😐
  2. You’re replacing an out-of-the-box page. What happens to your changes if a CU updates that page? :-/
  3. The change affects all web applications ran on the WFE server you make the change on. What if you have a web application you don’t want to auto-select a provider?

My first issue was disappointing. My second issue was risky. My third issue? Just not acceptable because we do have multiple web applications and don’t want all of them to auto-select. (more…)

Read Full Post »

My thanks to Peter Holpar for his blog post on “How to Override the Default Item Save Event Using Custom Fields.” Gave me the information I needed to create a handy custom field.

During our testing, though, we found that save errors were not being handled correctly. For example, when the “Allow duplicate values” was set to “No” and you tried to set a duplicate value in the field, you got a very ugly error page. So, I went back to Peter’s blog: No solution. I searched all over: no solution. So, back to Peter’s blog. There was one little tidbit that turned into the key to solving the problem and getting SharePoint to properly handle the SPDuplicateValuesFoundException. Peter mentioned an alternative to using SPContext.Current.ListItem.Update() was to use SaveButton.SaveItem(). Close, very close! The parameterless method is actually not available to our custom field, but SaveButton.SaveItem(SPItemContext, bool, string) is. Calling this method instead of the update method took care of the problem. Now, when someone tries to enter a duplicate value in our custom field, we get the nice, red message “This value already exists in the list.”

Now a happy Monday morning!

Read Full Post »

Sometimes, the old ways just work cleaner. After struggling for far longer than I should have to get files to post to my document library using the REST interface, I turned back the clock and looked at how I had done it in times past:

private static void PostFile(string url, byte[] bytes)
{
  HttpWebRequest r = (HttpWebRequest)WebRequest.Create(url);
  r.Credentials = CredentialCache.DefaultNetworkCredentials;
  r.Method = "PUT";
  byte[] buffer = new byte[1024];
  using (Stream stream = r.GetRequestStream())
  {
    using (MemoryStream ms = new MemoryStream(bytes))
    {
      for (int i = ms.Read(buffer, 0, buffer.Length); i > 0; i = ms.Read(buffer, 0, buffer.Length))
      {
        stream.Write(buffer, 0, i);
      }
    }
  }
  WebResponse resp = r.GetResponse();
  resp.Dispose();
}

The url parameter that you pass to this method is simply the URL the document will have after being posted. Example: http://sppaule/sites/test/Shared%20Documents/TestFolder/test.txt (this example would put the file “test.txt” in the TestFolder folder of the Shared Documents library in the “test” site). The bytes parameter contains the byte[] of the file to upload.

 
“More isn’t always better, Linus. Sometimes it’s just more.” (Anyone name that movie?)

 

Read Full Post »

Warning: Fluffy explanation content ahead! If you want to skip the background info and get straight to the meat of this post, skip the next few paragraphs.

I would like to start this post by giving credit where credit is due. A few years ago, Mark Wilson wrote a blog post on how to use calculated fields to control the coloring of calendar items in the calendar view with some custom JavaScript (see SharePoint 2010 Colour Calendar post SP1 update). Without thinking too terribly much about how it was put together or worked, I put it into play with a solution that used event receivers to update a Single Line Text field to the values needed for Mark’s script (a Calculated field was too simple to meet the full business requirements). Worked beautifully!

Upon testing the solution in SharePoint 2013, I found it didn’t work. And, not just a little: Badly! No calendar events would show on the calendar and the ribbon would get stuck in “Loading…” So, I returned to Mark’s blog and was happy to find his update, SharePoint 2013 Colour Calendar. Reading through the comments, I was surprised to see a stream of people mentioning Mark’s script wasn’t working shortly followed by Mark’s “I fixed it” reply. Something was wrong here that needed a deeper look.

Turns out, Mark has been copying the code from Microsoft’s scripts and appending a line to execute his ColourCalendar function. That works just fine until Microsoft modifies their scripts–which is precisely what has been happening frequently in SharePoint Online (o365). Even his 2010 version of the scripts had to handle the pre- and post-SP1 releases differently. (No fault of Mark. The changes were just too drastic.) I, for one, wasn’t interested in getting into a “test and modify” loop every time I needed to deploy a Cumulative Update. Not only that, but the scripts Mark provides are UI specific. I wanted–and pretty much needed (see my last post)–one script that could handle the different UIs by itself and not be subject to continual necessary updates because of changes made by Microsoft.

There were two key elements to meeting my requirements to handle the UI versions and cumulative updates without troubles:

  1. _spPageContextInfo.webUIVersion: This allows us to test which UIVersion is being used by the page. Important when you consider that the DOM changes quite considerably with each UI version.
  2. Extending JavaScript functions without overwriting them: This allows us to insert our own function call without needing to copy the original function’s code. (My thanks to “xdazz” for the answer to this one on Stack Overflow.)

Integrating all of this together with the Module Pattern best practice gained from Scot Hillier’s SPC2012 session, “A Primer in HTML5 and JavaScript,” I came up with a new script that can be used in UI version 4 or 15 in SharePoint 2013.

Script source: DynamicCalendarColor.docx

To use the script source, download it and save the contents as text-only as ‘DynamicCalendarColor.js’ (you can use a different name, but be sure to modify the last line accordingly). From there, follow Mark’s rather well written instructions on SharePoint 2013 Colour Calendar. Be sure your page also loads the jQuery libraries! (I don’t include that in my script because jQuery is often used in many other solutions, as well. Don’t want to be loading it twice.)

There you go: You’re now a SharePoint Calendar Artiste!
 

Read Full Post »

Spring cleaning is coming early to my SharePoint 2010 farm. With SharePoint 2013 right around the corner and a huge push to cleanup farms before doing the upgrade, I’ve taken the initiative to take out the garbage which has been accumulating for several years. I’m finding all sorts of rubbish, too: Fab 40 (yes, we still had remnants of it installed and running in SharePoint 2010); other no-longer used or supported features; unused sites; unused SharePoint groups; groups with users who no longer exist; and even audiences that were created and never used. Just like at home where I’m doing the winter pruning and de-cluttering the basement, the garbage and recycle cans have been filled to the brim many a time during this cleanup project. Hopefully, I’ll soon be able to blog about removing some of the other rubbish mentioned, but today I want to focus on those unused audiences.

I’ve ran across several questions posted on various forums about finding where audiences are being used or even how they are stored, but the answers given are generally missing or, at best, vague. Being that I needed a real solution, I got to task. The answer wasn’t simple: Audiences are used and stored in three different ways (that I’ve been able to identify–please let me know if I missed any!):

  1. You can specify an audience on a web part.
  2. You can set an audience on a list item.
  3. You can designate an audience for a navigation item (i.e. Top Nav Bar, Quick Launch, Global Navigation, or Current Navigation).

Thus, to find where an audience is used, you have to check all of these different areas. Not even the databases store this type of data in one place (see “Where Target Audience settings data is stored in SharePoint 2010?” on the Microsoft forum for more info on the storage of a SPListItem’s audience).

At this point, I’ve got to say, “I love PowerShell!” It comes to the rescue here, yet again. (Without it, I’d be relegated to writing a console app.) After studying and understanding the below PowerShell script, copy and paste it into a file (I named mine “Get-AudienceTargets.ps1” and saved it on the desktop).

Param(
	[parameter(Mandatory=$True,Position=1)]
	[String]
		$WebApplicationURL
)

if((Get-PSSnapin -Name Microsoft.SharePoint.PowerShell -ErrorAction
SilentlyContinue) -eq $null){
    $ver = $host | select version
    if ($ver.Version.Major -gt 1) {
        $Host.Runspace.ThreadOptions = "ReuseThread"
    }
    Add-PsSnapin Microsoft.SharePoint.PowerShell
    Set-location $home
}

$psShared = [System.Web.UI.WebControls.WebParts.PersonalizationScope]::Shared;

$allSites = Get-SPWebApplication $WebApplicationURL | Get-SPSite -Limit ALL;

[Void][System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.Audience");
$spcontext = [Microsoft.Office.Server.ServerContext]::GetContext($allSites[0]);
$audmanager = New-Object Microsoft.Office.Server.Audience.AudienceManager($spcontext);
$audiences = $audmanager.Audiences;

function GetAudienceNames([string]$audienceFilter){
    $au = $audienceFilter.Split(";;");
    if($au[0] -ne "") {
        $au[0].Split(',') | foreach-object { 
            if($audiences.AudienceExist([Guid]$_)) {
                $audienceFilter = $audienceFilter.Replace($_, $audiences[[Guid]$_].AudienceName); # $r += $audiences[[Guid]$_].AudienceName + ", ";
            }
        }
    }
    return $audienceFilter.Replace(";;","|");
}

function GetLWPM($w, [string]$url){
    try {
        $wpm = $w.GetLimitedWebPartManager($url, $psShared);
    }
    catch {
        write-host ("Error getting limited web part manager for " + $url) -foreground red
    }
    return $wpm;
}

write-output ("Web|Path|Web Part Title|SharePoint Audience|Audience AD LDAP Path|SharePoint Group");

$allSites | get-spweb -limit all | foreach-object {
	$w = $_;
    try {
	$w.Files | where-object {$_.ServerRelativeUrl -like "*.aspx"} | foreach-object {
		$wpm = GetLWPM -w $w -url $_.ServerRelativeUrl; #$w.GetLimitedWebPartManager($_.ServerRelativeUrl, $psShared);
        if($wpm){
            try{
		      $wpm.WebParts | where-object {$_.AuthorizationFilter -gt ""} | foreach-object {
		          write-output ($w.Url + "|" + $_.ServerRelativeUrl + "|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.AuthorizationFilter));
		      }
            } finally {
                $wpm.Dispose();
            }
        }
	}
	$w.Lists | where-object { !$_.Hidden } | foreach-object {
        if($_ -is [Microsoft.SharePoint.SPDocumentLibrary]){
    		$_.Items | where-object {$_.Url -like "*.aspx"} | foreach-object {
                $item = $_;
                $iUrl = ($w.Url + "/" + [System.Web.HttpUtility]::UrlPathEncode($_.Url));
    			$wpm = GetLWPM -w $w -url $iUrl; # $w.GetLimitedWebPartManager($iUrl, $psShared);
                if($wpm){
                  try{
    			    $wpm.WebParts | where-object {$_.AuthorizationFilter -gt ""} | foreach-object {
    			 	   write-output ($w.Url + "|" + $item.Url + "|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.AuthorizationFilter));
    			    }
                  } finally {
                    $wpm.Dispose();
                  }
                }
            }
        }
        if ($_.Fields.ContainsFieldWithStaticName("Target_x0020_Audiences")) {
            $_.Items | foreach-object {
                if ($_["Target_x0020_Audiences"] -ne $null){
                    write-output ($w.Url + "|" + $_.Url + "|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_["Target_x0020_Audiences"]));
                }
            }
        }
	}
    if([Microsoft.SharePoint.Publishing.PublishingWeb]::IsPublishingWeb($w)){
        $pweb = [Microsoft.SharePoint.Publishing.PublishingWeb]::GetPublishingWeb($w);
        $pweb.Navigation.GlobalNavigationNodes | where-object { $_.Properties.ContainsKey("Audience") -and $_.Properties["Audience"] -ne "" } | foreach-object { 
            write-output ($w.Url + "|[GlobalNavigationNodes]|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.Properties["Audience"]))
        }
        $pweb.Navigation.CurrentNavigationNodes | where-object { $_.Properties.ContainsKey("Audience") -and $_.Properties["Audience"] -ne "" } | foreach-object { 
            write-output ($w.Url + "|[CurrentNavigationNodes]|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.Properties["Audience"]))
        }
    } else {
        $w.Navigation.QuickLaunch | where-object { $_.Properties.ContainsKey("Audience") -and $_.Properties["Audience"] -ne "" } | foreach-object { 
            write-output ($w.Url + "|[QuickLaunch]|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.Properties["Audience"]))
        }
        $w.Navigation.TopNavigationBar | where-object { $_.Properties.ContainsKey("Audience") -and $_.Properties["Audience"] -ne "" } | foreach-object { 
            write-output ($w.Url + "|[TopNavigationBar]|" + $_.Title + "|" + (GetAudienceNames -audienceFilter $_.Properties["Audience"]))
        }
    }
    } finally {
        $w.Dispose();
    }
}

Next, execute the script from a PowerShell command prompt (the astute may notice this doesn’t necessarily have to be the SharePoint Management Shell–when writing scripts, I like to use the PowerShel ISE but it doesn’t normally load the SharePoint snapin), supplying the URL of the web application you want to check.

The script will iterate through all of the content in the web application and output pipe-delimited (‘|’) text with a header and the data for the audience data it finds such as:

Web|Path|Web Part Title|SharePoint Audience|Audience AD LDAP Path|SharePoint Group
http://sppaule|Shared Documents/Test-WebPartPage.aspx|Shared Documents|Admins||
http://sppaule|SitePages/Test-WikiPage.aspx|Shared Documents|Admins||
http://sppaule/blank|Lists/Tasks/2_.000|Test2|Admins||
http://sppaule/sites/hp|Pages/default.aspx|Content Editor|7788a621-ec5a-4656-8976-537e29a6d85c,0223cb9a-f465-4598-ae0f-84bc590e0b53,eab8c97d-fcdf-427a-93f7-a929ce24a589,320bc09a-31fb-4350-87d1-37d2cdffaf01||
http://sppaule/sites/hp|Pages/default.aspx|Utah Weather|Admins||
http://sppaule/sites/hp|[GlobalNavigationNodes]|Dev||CN=Team PaulE,OU=Distribution Groups,OU=Corp,DC=ewert,DC=fam
CN=Gen1,OU=Distribution Groups,OU=Corp,DC=ewert,DC=fam|Gen2
http://sppaule/sites/hp|[GlobalNavigationNodes]|HR|0a16b96d-b445-4f05-a6fd-0db9a31fcbad||
http://sppaule/sites/hp|[GlobalNavigationNodes]|Teams|0a16b96d-b445-4f05-a6fd-0da9d31fcbad||
http://sppaule/sites/hp|[CurrentNavigationNodes]|Lists|Admins||

There you have it: A listing of where audiences are used! Those GUIDs that are remaining in the “SharePoint Audience” column of the output are audiences that no longer exist. You’ll need to use the information provided to navigate to the content and update the audience settings for that object (list item, navigation item, or web part).

I like to capture the output to a text file (in the case below, the script will check the web application at http://sppaule for audiences and output will be stored in a file named “audTargets.txt” on my desktop):

PS C:\Users\sppaule\Desktop> .\Get-AudienceTargets.ps1 http://sppaule > audTargets.txt

Then, I open that file in Microsoft Excel, delimiting the columns on the ‘|’ character for easier reading.

I will note that I get a few “log4net” errors in the console window, but these aren’t included in the captured output and I’m not presently concerned with them because they’re probably coming from some of my other garbage. Also, you probably don’t want to execute this during your peak usage periods. Since it iterates through most of your content, it’s pretty intense and can take a long time to execute on farms with a lot of content.

Happy audience hunting!

Read Full Post »

Older Posts »