Showing posts with label Development. Show all posts
Showing posts with label Development. Show all posts

Sunday, July 12, 2015

Configuring metadata defaults with CSOM

SharePoint folders have had it rough the last couple of years. The internet is full of 'folders are evil' posts, but in my view folders still have their uses. One of the interesting features SharePoint folders bring to the table is automatically setting metadata. Per folder you can configure which default values fields should have. This can be a great enabler when people don't want to tag documents manually.

We wanted to use this in a recent project where we had the requirement to provision and configure this from code. The metadata defaults objects however are only available in the full trust object model, and we build for SharePoint online. That leaves us with the puzzle of what actually happens when you configure metadata defaults and figuring out if we can do the same from CSOM. We found that metadata defaults functionality is implemented through two components:

  • The 'client_LocationBasedDefaults.html' file in the forms folder of the document library
  • A feature receiver bound to the same document library


The following screenshots are from an environment that's been configured manually. We have our document library 'Metadata'. This library has two folders, 'Yes' and 'No'. A choice field with the same options is added to the library and has been configured to be set automatically for files that are added to the folders.


With SharePoint designer we can view the contents of the 'client_LocationBasedDefaults.html'. It's basically a mapping of the folder (url), the field to be set, and the field value to be set. All this in a very simple XML structure.

The event receiver can be spotted using the great tool SharePoint Manager 2013. It's interesting to note that the event receiver is part of the "Microsoft.Office.DocumentManagement" namespace, the same namespace that gives us the Document Set which is also great folder and metadata tool.

Well, this doesn't appear to be to hard to do through CSOM! First we need to create the metadata file. The hidden forms folder in the library has the interesting property that you can add files to it just as you would do to any other folder. The following snippet is all you need:
// Creating the LocationBasedDefaults file in the forms folder. Will overwrite if it is already there.
var formsFolder = ctx.Web.GetFolderByServerRelativeUrl("/Metadata/Forms/");
var fci = new FileCreationInformation();
fci.Content = Encoding.UTF8.GetBytes("<MetadataDefaults><a href=\"/Metadata/No\"><DefaultValue FieldName=\"YesNo\">No</DefaultValue></a><a href=\"/Metadata/Yes\"><DefaultValue FieldName=\"YesNo\">Yes</DefaultValue></a></MetadataDefaults>");
fci.Url = "client_LocationBasedDefaults.html";
fci.Overwrite = true;
var metaDataFile = formsFolder.Files.Add(fci);

ctx.Load(metaDataFile);
ctx.ExecuteQuery();


After this we need to bind the out of the box event receiver. Note that we explicitly configure the event receiver to be synchronous. If you don't, the user will need to refresh the screen after dragging a document to the library to see the updated value. The following snippet has all the code required for this:
//Binding the OOTB event receiver
var list = ctx.Web.Lists.GetByTitle("Metadata");
var erci = new EventReceiverDefinitionCreationInformation();

erci.ReceiverName = "LocationBasedMetadataDefaultsReceiver ItemAdded";
erci.SequenceNumber = 1000;
erci.ReceiverClass = "Microsoft.Office.DocumentManagement.LocationBasedMetadataDefaultsReceiver";
erci.ReceiverAssembly = "Microsoft.Office.DocumentManagement, Version=15.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c";
erci.EventType = EventReceiverType.ItemAdded;
erci.Synchronization = EventReceiverSynchronization.Synchronous;

var receiver = list.EventReceivers.Add(erci);
ctx.Load(receiver);
ctx.ExecuteQuery();


And that's all there is to it. It's also possible to do this with Managed Metadata values. However, to get these to work you need to ensure they're already in the taxonomy hidden list on the site you are deploying this to. That's slightly out of scope for this write-up, but let me know if you run into trouble with that.

Attached: A full demo project for a provider hosted app which creates and configures a document library with managed metadata defaults.

Friday, September 27, 2013

Checking for changed properties of an SPWeb

The SPWeb object is one of the places where a lot of the SharePoint magic happens. Many changes performed through the UI or through features are persisted there. When refactoring behavior it's often useful to compare the state of the SPWeb object before and after an update. I've created a small powershell scrip to facilitate this. It reads all the properties of the SPWeb and all the properties in the property bag. It then pauses, allowing you to do whatever you have to. Then the script gets the properties again and outputs the differences. Let me know if it helps you.

Of course, all the regular legal stuff applies. Use at your own risk. I do not accept any liability for what happens when you run this script. And never, never, run a random script you've downloaded from the internet against a production environment without properly testing and examining it.


param(
 [string]$WebUrl = $(throw "WebUrl required.")
)
 
#Region [ Load Assemblies ]
$spNotFoundMsg = "SharePoint not found. Run this script from a SharePoint server that is part of the farm where you want to update your content types"
 
if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") -eq $null)     { throw $spNotFoundMsg; }
if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") -eq $null)    { throw $spNotFoundMsg; }
if ([Reflection.Assembly]::LoadWithPartialName("System.Xml.Linq") -eq $null)      { throw $spNotFoundMsg; }
if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") -eq $null)    { throw $spNotFoundMsg; }
if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") -eq $null) { throw $spNotFoundMsg; }
if ([Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Taxonomy") -eq $null)   { throw $spNotFoundMsg; }
 
$snapin = Get-PSSnapin | Where-Object {$_.Name -eq 'Microsoft.SharePoint.Powershell'}
if ($snapin -eq $null)
{
   Write-Output "Loading Microsoft SharePoint Powershell Snapin"
   Add-PSSnapin "Microsoft.SharePoint.Powershell"
} 
#endregion 

$web = get-SPWeb $WebUrl

$oldProperties = $web.AllProperties.Clone()

$web | % { foreach ($property in $_.PSObject.Properties) { $oldProperties.Add("SPWeb." + $property.Name, $_.PSObject.properties[$property.Name].Value -as [string]) }}

$web.Dispose()

Write-Host "We have grabbed a copy of the properties as they are now. Make your change, and press any key to continue.."

While ($KeyInfo.VirtualKeyCode -Eq $Null) {
        $KeyInfo = $Host.UI.RawUI.ReadKey("NoEcho, IncludeKeyDown")
}

$web = get-SPWeb $WebUrl

$newProperties = $web.AllProperties.Clone()
$web | % { foreach ($property in $_.PSObject.Properties) { $newProperties.Add("SPWeb." + $property.Name, $_.PSObject.properties[$property.Name].Value -as [string]) }}


$updates = 0
$additions = 0

foreach ($h in $newProperties.Keys) {
    
    $newValue = $newProperties.Item($h)

    if ($oldProperties.Keys.Contains($h))
    {
        $oldValue = $oldProperties.Item($h)
        if ($oldValue -ne $newValue)
        {
            $updates++
            Write-Host "`nFound an update in the property '$h'.`n'$oldValue'`nbecame`n'$newValue'"
        }
    }    
    else
    {
        $additions++
        Write-Host "`nFound an new property '$h', with the following value:`n'$newValue'"
    }
}
Write-Host "Found $updates updates and $additions additions"
$web.Dispose()

Wednesday, March 30, 2011

Deploying solutions to a specific Web Application

SharePoint solutions (non-sandboxed) can be deployed at two scopes;
  • Globally
  • Web Application

SharePoint has the annoying habit of forcing you to deploy globally whenever possible. When you try to deploy a global solution to a specific url you get the following message:
- This solution contains no resources scoped for a Web application and cannot be deployed to a particular Web application.

In almost all cases I want to deploy my solution to a single web application. This makes creating reusable deployment scripts much easier. It also adds logic to your deployment scenario's and farm solution overview in Central Administration.

Update
Another important reason to deploy to a single web application is that upon deployment, retraction or updating of your solution you can avoid restarting all application pools. This limits downtime, especially when you are not alone in your farm:
Avoid creating a lot of global SharePoint Packages and try instead to provision as much as you can to specific Web Applications. Every time you touch a global SharePoint Package all Applications Pools will be stopped/recycled. Although there are some scenarios when you can’t avoid creating global SharePoint Packages, you should try to avoid them
An interesting post by Waldek on the subject.
/Update

The trick is fooling SharePoint into registering your solution as a web application scoped solution. SharePoint checks wether there are items which have to be deployed to a specific web application when your solution is added to the solution gallery. One of the elements that SharePoint checks for are safe control entries. These have to be merged into a web.config for a specific web application. The easiest way to force deployment to a specific web application is adding a dummy safe control entry to your package.

Here is how you do that:



1. Double click the package
2. Open the manifest
3. Edit the options
4. Add you dummy data. In my solution I added the following:

<Solution xmlns="http://schemas.microsoft.com/sharepoint/"> 
 <Assemblies>   
  <Assembly Location="SharePointProject1.dll" DeploymentTarget="GlobalAssemblyCache">     
   <SafeControls>       
   <SafeControl Assembly="SharePointProject1,Version=1.0.0.0, Culture=neutral, PublicKeyToken=****************" Namespace="SharePointProject1" TypeName="*" />     
   </SafeControls>   
  </Assembly> 
 </Assemblies>
</Solution>

That's all there is to it. This solution will now only deploy at the web application scope!


Note: For MOSS this works the same. How to add the entry is a bit different depending on your wsp packaging tool. With STSDEV you can add the entry to the SolutionConfig.xml.

Friday, September 5, 2008

ListTemplate name attribute

Today I got this error again:
Exception from HRESULT: 0x81070201 at Microsoft.SharePoint.Library.SPRequestInternalClass.CreateListFromFormPost(String bstrUrl, String& pbstrGuid, String& pbstrNextUrl)
at Microsoft.SharePoint.Library.SPRequest.CreateListFromFormPost(String bstrUrl, String& pbstrGuid, String& pbstrNextUrl)

I encountered this one previously, but I forgot how to solve it. Therefore I'll post it here, so next time I won't spend an hour on this.

The name attribute in ListTemplate must be the subfolder scheme.xml is in. Else, when you create a listinstance, you get an error.

This posting got me through it this time:
http://spdummies.blogspot.com/2008/01/hresult-0x81070201-exception.html

Monday, August 18, 2008

Enabling Item Scheduling through code

Many settings in MOSS are reachable through code. You could say all, because all settings you see in the GUI are based on actions performed by code. However, the translation is not always very transparant.

A library can have item scheduling enabled. This allows you to specify a window in time in which a page should be visible to the world. When you enable item scheduling through the GUI you first have to enable "Minor versions" and "Moderation" in the versioning settings. This is quite easy in code:

//pageLibrary is a SPList object. I assume you know how to get to it.
pageLibrary.EnableMinorVersions = true;
pageLibrary.EnableModeration = true;
pageLibrary.Update();

The next part is where you hit a brick wall. There is no "Manage Item Scheduling" property for list objects. What shows as a checkbox in the GUI hides a couple of things that happen when you turn Item Scheduling on. The most important is that EventReceivers are added to the list. Normally, when working with EventRecievers you're making your own. Here the objective is to add Microsoft's own EventReceivers through code:

pageLibrary.EventReceivers.Add(SPEventReceiverType.ItemAdded, "Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c", "Microsoft.SharePoint.Publishing.Internal.ScheduledItemEventReceiver");
pageLibrary.EventReceivers.Add(SPEventReceiverType.ItemUpdating, "Microsoft.SharePoint.Publishing, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c", "Microsoft.SharePoint.Publishing.Internal.ScheduledItemEventReceiver");

Thats it! Seems easy enough, don't you think? These 5 lines of code made up a days work though.

Friday, August 15, 2008

CAML filtering using a time

CAML is the query language used to get content from libraries and lists in a SharePoint environment. The keywords are reminiscent of SQL, but they have been formatted to be XML. This means they are very hard to read and write. This is why many use the great (and free) u2u CAML builder.

There are two pittfalls I dove into headfirst recently. When you create a custom control in c# you can specify a query to be executed on a SPList object. Easiest is to create your query in u2u and then copy-paste it into your code. However, you should remove the <query> and </query> tags from your query. If you don't, the result will be all items from the list. Your query will do nothing. This can be quite hard to spot if you expected most or all your items to be returned.

The second pittfall was in my query parameters. I wanted all items where a DateTime field was lower then "now". Now can be specified as
<value type="DateTime"><today /></value>
or you can get your code to output something like this:
<value type="DateTime">2008-08-08T16:34:07Z</value>

This will work. Kinda. It may take some time before you notice that the query processes the date just fine. However, the time part will be ignored. The second example effectively filters like it was:
<value type="DateTime">2008-08-08T00:00:00Z</value>

If you want the time value to be used you've got to say:
<value type="DateTime" includedatetime="'TRUE'"><Today /></value>


Note: I used both the <Today /> and the yyyy-mm-ddThh-mm-ssZ format in my examples. Both exhibit the same behaviour on dates.

Thursday, August 7, 2008

"local device name is already in use"

There are quite a number of things you could be doing wrong when you're facing this error message. A quick Google search will show a lot of them. One thing Google doesn't mention is that it can also be related to a duplicate GUID that is used by two custom site columns and referenced by a feature reciever.


In my defense, maybe I was just recycling GUIDs out of fear I might someday run out... Or something like that.

CQWP and custom column troubles

Last week I've been struggling with one of the most deceptive bugs I've ever encountered. I had created a custom site column, a date-time field. Multiple contenttypes (all pages) used this site column. Then I used a content by query webpart to show a list of pages on the site, filtered by my custom date field. The first time I tried this I got no results. I thought something had gone wrong in the declaration of my columns and types, so I made some minor modifications and tried again. It worked, I got results and I proudly published my work.

One day later, a co-worker complained that the results of the CQWP were incorrect. I checked, and he was right. Some results should not have been shown but were, and some that should have been shown were missing. More than a day I spent on this until I found this great posting by Ranjan Banerji (the fact that the link reads "CQWP nightmares" should be a hint):
http://techblog.ranjanbanerji.com/post/2007/10/30/Content-Query-Web-Part-(CQWP)2c-Cross-List-Query-Nightmare-Part-3.aspx

What happens is that the custom date field gets assigned some generic name in the SQL database. This name is then used in the query by the CQWP. However, there is no guarantee that this name is the same in every content type that uses the custom site column. So the query could be right for only one of your contenttypes. It could also work for most. Maybe even all. But you might not notice you're missing some results until someone else points it out to you...

Rating