tag:blogger.com,1999:blog-13536049014048902692024-02-19T18:06:05.875+01:00The SPCodeCollectionThe SPCodeCollection - by Thomas JorgensenThomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.comBlogger20125tag:blogger.com,1999:blog-1353604901404890269.post-28062616312401642392014-09-23T11:13:00.000+02:002014-09-23T11:13:21.520+02:00Debugging Powershell Cmdlets (C#)<div class="my-summary">
When building Cmdlets in C# for Powershell one of the things you typically want to do is hit F5 to debug your Cmdlet code.<br />
Fortunately this is quite easy to set up. Below I will be showing how to do it in Visual Studio 2012 - but I expect the process to be nearly identical in other Visual Studio versions.</div>
<a name='more'></a><br />
You only need to do two things:<br />
<ol>
<li>Create a test script that loads and executes your cmdlet.</li>
<li>Set project to use Powershell as startup application, and the test script as startup argument.</li>
</ol>
<br />
<b>How I did it:</b><br />
<br />
I created a simple Powershell script file (test.ps1) that I placed in my project, and set Build Action to None and Copy to Output Directory to Always.<br />
The script points to the generated dll and loads it as a module. Then it calls a cmdlet in my project.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihnIdN2jSveM2lhveAViAKTAoMy1Np70OK1qSMUrV4RvZ1wWp8IVPq_wpdfRosSfew5fSHl3S7-h9xhqdISxg0_utidyn1sEvW6Jmt2EygOnsG8_yqGIYuiRQRM3zyEuiBalBeTe-TeePv/s1600/Blog.Cmdlets.Script.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEihnIdN2jSveM2lhveAViAKTAoMy1Np70OK1qSMUrV4RvZ1wWp8IVPq_wpdfRosSfew5fSHl3S7-h9xhqdISxg0_utidyn1sEvW6Jmt2EygOnsG8_yqGIYuiRQRM3zyEuiBalBeTe-TeePv/s1600/Blog.Cmdlets.Script.PNG" height="417" width="640" /></a></div>
<br />
Next, I opened project properties for the project, and I went to the Debug setting.<br />
Under Startup Action choose 'Start external program', and set it to 'c:\Windows\system32\WindowsPowerShell\v1.0\powershell.exe'.<br />
Under Start Options set Command line arguments to '-noexit .\test.ps1'.<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfGlyB3Z5JyxR7vPE0QxRWcK3u6ZOTCQFY4ErWwupx8e81awzjQSt3RvQ0Ou_q8zHTyURMd7YnWLKtNZ5-lI5SwfZ5KRtFDNfPwXrohOhjLQffW4OoT_fAbkMc8AZIN8BEHnVsWE3j7dkZ/s1600/Blog.Cmdlets.Debug.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjfGlyB3Z5JyxR7vPE0QxRWcK3u6ZOTCQFY4ErWwupx8e81awzjQSt3RvQ0Ou_q8zHTyURMd7YnWLKtNZ5-lI5SwfZ5KRtFDNfPwXrohOhjLQffW4OoT_fAbkMc8AZIN8BEHnVsWE3j7dkZ/s1600/Blog.Cmdlets.Debug.PNG" height="336" width="640" /></a></div>
<br />
<div class="separator" style="clear: both; text-align: center;">
</div>
<div>
Now, when I press F5 the solution builds, Powershell is launched, and myscript is run. I can set breakpoints in the Cmdlet, and it will get hit, allowing me to step through the code one piece at a time, and doing all the usual debugging stuff.<br />
<br />
Just for reference, here is the cmdlet that I used in the example:<br />
<br />
<div class="separator" style="clear: both; text-align: center;">
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgEUSgM6P1PxikgK5dZaRLAh0M3ilDC5Mmt8tDhCw0QBp46WwKxoVLVvz5co-rjuo2-KNz83xIbQAMvgpVKku53ZPH_e2p2Z1klYZE1jBXQ94epCmtEEo6zpXmeQDs5hxCGZwcwE6FwNGw/s1600/Blog.Cmdlets.Code.PNG" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"><img border="0" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjgEUSgM6P1PxikgK5dZaRLAh0M3ilDC5Mmt8tDhCw0QBp46WwKxoVLVvz5co-rjuo2-KNz83xIbQAMvgpVKku53ZPH_e2p2Z1klYZE1jBXQ94epCmtEEo6zpXmeQDs5hxCGZwcwE6FwNGw/s1600/Blog.Cmdlets.Code.PNG" height="416" width="640" /></a></div>
<br /></div>
Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-28518294322836930332014-02-07T09:20:00.000+01:002014-02-07T09:20:05.711+01:00SharePoint InfoPath List Form: Move from development to test and production environments<div class="my-summary">
Using InfoPath is a quick way to customize your Sharepoint list forms. But as with most quick solutions is comes with a cost:<br />
Often people will create the InfoPath form on their development or test environment, documenting the process, and then later they will repeat the customization steps on the production environment. And if you use the same form on several lists you have to do the above even more times.<br />
This process takes time, and is error prone. Fortunately there is an easy way to move the forms between lists and environments, and still associate the individual form with the correct list.</div>
<a name='more'></a><br />
When you have completed an InfoPath list form for SharePoint you will typically press the 'Publish' or 'Quick Publish' button. This will publish the form to a specific SharePoint list, with no option to change the publishing path.<br />
What we need to do is change the registrations in the InfoPath form that determines the SharePoint list and content type. This cannot be done in InfoPath Designer, but requires us to edit the form xml by hand. Fortunately the process is not difficult.<br />
The steps are as follows:<br />
<br />
<ol>
<li>Start by opening the customized SharePoint list form in InfoPath Designer.</li>
<li>Go to Files > Publish > Export Source Files, and select or create a folder on your local computer to export the file to.</li>
<li>Close InfoPath Designer to avoid locking of files.</li>
<li>Navigate to the folder and, using Notepad or similar, open the manifest.xsf file.</li>
<li>Find <b>sharePointListID</b> and replace the value with the ID for the new target list. You can get this by going to the settings page of the list, and look at the URL.</li>
<li>Find <b>contentTypeID</b> and replace the value with the ID for the 'same' content type in the new target list. You can find this by going to the content type from the list settings page. If you do not use content types you can temporarily enable management of content types, then disable it after you have the ID.</li>
<li>Find <b>siteURL</b> and replace the value with the path to the new target site. NOTE: If the structures of your development, test and production environments are the same you kan skip this step.</li>
<li>Find <b>relativeListUrl</b> and replace the value with the path to the new target list, relative to it's site. NOTE: If the structures of your development, test and production environments are the same you can skip this step.</li>
<li>Save the file and close Notepad (or whatever editor you use).</li>
<li>Right-click on the manifest.xsf file and select Design to open the form in InfoPath Designer.</li>
<li>Go to Files > Publish, and verify that the form now points to your new list. Then publish the form, and you are done.</li>
</ol>
<div>
If you get an error in InfoPath Designer for specific fields after changing the target list, then you probably have fields with different internal names. In that case you simply reassign the data bindings for the specific fields - InfoPath Designer will give you a listing of the available fields in the new list to choose from.</div>
<div>
<br /></div>
<div>
In a soon-to-come post I will supply a PowerShell script that handles the above, to make it even easier.</div>
Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com3tag:blogger.com,1999:blog-1353604901404890269.post-44132040377018910022012-08-31T10:31:00.000+02:002012-08-31T10:37:16.955+02:00How to: Set a date with correct time in a SharePoint list item<div class="my-summary"><p>When adding a SharePoint list item the one field type that has given me most problems is the SPDateTimeField:</p>First there is the question of entering the date in a format that SharePoint accepts. Then there is the problem of getting the time zone right.</p><p>Fortunately, neither of the two problems are actually hard to solve – Microsoft has tried to make life easy for us by supplying the tools we need to solve them quietly and efficiently.</p></div><a name='more'></a> <h2>Setting a valid date</h2><p>SharePoint accepts date entered in ISO8601 format. So all we need to do is to enter the date in the following format: </p><p><font face="Courier New">yyyy-MM-ddThh:mm:ssZ<br />
2012-08-22T07:40:31Z (example)</font></p><p>You could of course do a</p><p><font face="Courier New">string.Format(<br />
"{0:0000}-{1:00}-{2:00}T{3:00}:{4:00}:{5:00}Z",<br />
date.Year,<br />
date.Month,<br />
date.Day,<br />
date.Hour,<br />
date.Minute,<br />
date.Second);</font></p><p>but that is a bit cumbersome, and you are bound to sometimes get the formatting wrong. Luckily, Microsoft has provided us with the following method, which is found in the Microsoft.SharePoint.Utilities namespace of the Microsoft.SharePoint assembly:</p><pre style="line-height: normal; font-family: ; background: white; color: "><font face="Consolas"><span style="color: "><font color="#2b91af">SPUtility</font></span><font color="#000000">.CreateISO8601DateTimeFromSystemDateTime(date);
</font></font></pre><p>This creates a string with the correct format. There is also the opposite method that creates a DateTime object:</p><p><font face="Consolas"><span style="color: "><font color="#2b91af">SPUtility</font></span><font color="#000000">.CreateDateTimeFromISO8601DateTimeString(dateString); </font></font></p><p>Unfortunately, if you use the above on a random DateTime object to set the date and time of a list item, you will probably see that the time part is offset by a number of hours. This can happen even if you get the DateTime value directly from the same list item that you are inserting it into.</p><h2>Getting the time part correct</h2><p>The problem with the time part offset is of course a matter of time zones. There are several places we can fiddle with the time zone, including on the web application and the individual site. But there is a (again Microsoft-provided) simpler way:</p><pre style="line-height: normal; font-family: ; background: white; color: "><font color="#000000" face="Consolas">web.RegionalSettings.TimeZone.LocalTimeToUTC(date);
</font></pre><p>This creates a DateTime object with the correct time zone. If you insert this object in the CreateISO… method above, you will get a string that SharePoint interpretes as having the correct time zone. As before, the opposite method also exists:</p><pre style="line-height: normal; font-family: ; background: white; color: "><font color="#000000" face="Consolas">web.RegionalSettings.TimeZone.UTCToLocalTime(date);
</font></pre><h2>Summary</h2><p>Setting date and time is always tricky, due to the many different formats and time zones. In the SharePoint object model we have methods that lets us handle these issues in a standardized manner. These methods are available both in the full object model, and in the limited sandboxed object model.</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com3tag:blogger.com,1999:blog-1353604901404890269.post-67159204066242089242012-08-13T07:07:00.001+02:002013-05-15T09:18:44.842+02:00How to get HTML5 content in SharePoint 2010<div class="my-summary">
Suppose you want to use HTML5 elements on your SharePoint 2010 site? You create a web part that renders the content as HTML5, for example using <canvas> or <svg> to render dynamic graphical content. The graphics show up fine in FireFox, Chrome etc, but in Internet Explorer it doesn't appear at all, or it is rendered incorrectly. You double-check to ensure that you are using Internet Explorer 9 or later, which is HTML5 compatible. Still the problem persists. How do we solve this?</div>
<a name='more'></a><br />
The reason the HTML5 content is not rendered correctly is that the master page contains an instruction to Internet Explorer to act as if it was version 8, even if you have version 9 (or later). So the solution is to change this instruction.<br />
<br />
Open the v4.master master page for your site using a text editor or SharePoint Designer. In the <span class="my-code-inline"><head></span> section, locate the line:<br />
<div class="my-code">
<head runat="server"><br />
...<br />
<meta http-equiv="X-UA-Compatible" content="IE=8"/><br />
...<br />
</head></div>
and change it to<br />
<div class="my-code">
<head runat="server"><br />
...<br />
<meta http-equiv="X-UA-Compatible" content="IE=9"/><br />
...<br />
</head></div>
Now your HTML5 content will render correctly. This solution works both for SharePoint Online and your friendly neighborhood SharePoint.Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com1tag:blogger.com,1999:blog-1353604901404890269.post-13007672682718733772012-04-22T08:01:00.001+02:002012-06-29T09:28:35.925+02:00SharePoint Client: How to get item attachments<div class="my-summary">SharePoint item attachments are an easy way to handle files associated with list items. In this post I will demonstrate how to get to the attachments through client-side code.<br />
<br />
The SharePoint Client Object Model (SPCOM) allows us to work with ShaePoint from the client side. SPCOM is basically a web service wrapper, exposing much of the existing web services functionality through nice classes. A few features are unfortunately either missing or just inconvenient through the SPCOM, in which case we need to revert to the old web services way. List item attachments is such a case.</div><a name='more'></a><br />
To get a list of attachments for a given list item in SharePoint from the client-side we have two options:<br />
<ol><li>Use the SharePoint Client Object model<br />
</li>
<li>Use the built-in web services directly</li>
</ol><h2>Option #1: Using the SharePoint Client Object Model</h2>Attachments are basically files that reside in a folder in a specific location:<br />
<site URL>/<list root folder>/Attachments/<item id><br />
To get to there through the clint object model we need to start at the web level, then get a reference to the list, then the root folder, then the attachments folder, then the item folder, and finally we can get to the files. The code to achieve this is:<br />
<pre style="background: white; line-height: normal;"><span style="font-family: Consolas;"><span style="color: blue;">public</span><span style="color: black;"> </span><span style="color: #2b91af;">IEnumerable</span><span style="color: black;"><</span><span style="color: blue;">string</span></span><span style="font-family: Consolas;"><span style="color: black;">> GetAttachemntUrls(
SP.</span><span style="color: #2b91af;">ClientContext</span></span><span style="font-family: Consolas;"><span style="color: black;"> ctx,
</span><span style="color: #2b91af;">Guid</span></span><span style="font-family: Consolas;"><span style="color: black;"> listId,
</span><span style="color: blue;">int</span></span><span style="font-family: Consolas;"><span style="color: black;"> itemId)
{
</span><span style="color: #2b91af;">List</span><span style="color: black;"><</span><span style="color: blue;">string</span><span style="color: black;">> urls = </span><span style="color: blue;">new</span><span style="color: black;"> </span><span style="color: #2b91af;">List</span><span style="color: black;"><</span><span style="color: blue;">string</span></span><span style="font-family: Consolas;"><span style="color: black;">>();
ctx.Load(ctx.Web, w => w.Lists);
SP.</span><span style="color: #2b91af;">List</span></span><span style="font-family: Consolas;"><span style="color: black;"> list = ctx.Web.Lists.GetById(listId);
ctx.Load(list, l => l.RootFolder);
SP.</span><span style="color: #2b91af;">Folder</span></span><span style="font-family: Consolas;"><span style="color: black;"> rootFolder = list.RootFolder;
ctx.Load(rootFolder, rf => rf.Folders);
SP.</span><span style="color: #2b91af;">FolderCollection</span></span><span style="font-family: Consolas;"><span style="color: black;"> folders = rootFolder.Folders;
ctx.Load(folders);
ctx.ExecuteQuery();
</span><span style="color: blue;">foreach</span><span style="color: black;"> (SP.</span><span style="color: #2b91af;">Folder</span><span style="color: black;"> folder </span><span style="color: blue;">in</span></span><span style="font-family: Consolas;"><span style="color: black;"> folders)
{
</span><span style="color: blue;">if</span><span style="color: black;"> (folder.Name == </span><span style="color: #a31515;">"Attachments"</span></span><span style="font-family: Consolas;"><span style="color: black;">)
{
SP.</span><span style="color: #2b91af;">FolderCollection</span></span><span style="font-family: Consolas;"><span style="color: black;"> attachmentFolders = folder.Folders;
ctx.Load(attachmentFolders);
ctx.ExecuteQuery();
</span><span style="color: blue;">foreach</span><span style="color: black;"> (SP.</span><span style="color: #2b91af;">Folder</span><span style="color: black;"> itemFolder </span><span style="color: blue;">in</span></span><span style="font-family: Consolas;"><span style="color: black;"> attachmentFolders)
{
</span><span style="color: blue;">if</span></span><span style="font-family: Consolas;"><span style="color: black;"> (itemFolder.Name == itemId.ToString())
{
SP.</span><span style="color: #2b91af;">FileCollection</span></span><span style="font-family: Consolas;"><span style="color: black;"> files = itemFolder.Files;
ctx.Load(files);
ctx.ExecuteQuery();
</span><span style="color: blue;">foreach</span><span style="color: black;"> (SP.</span><span style="color: #2b91af;">File</span><span style="color: black;"> file </span><span style="color: blue;">in</span></span><span style="font-family: Consolas;"><span style="color: black;"> files)
{
urls.Add(file.ServerRelativeUrl);
}
</span><span style="color: blue;">break</span></span><span style="font-family: Consolas;"><span style="color: black;">;
}
}
</span><span style="color: blue;">break</span></span><span style="font-family: Consolas;"><span style="color: black;">;
}
}
</span><span style="color: blue;">return</span><span style="color: black;"> urls;
}
</span></span></pre>You now have a list of server-relative attachment URLs.<br />
In the code above I have added a reference to the client library like this:<br />
<pre style="background: white; line-height: normal;"><span style="font-family: Consolas;"><span style="color: blue;">using</span><span style="color: black;"> SP = Microsoft.SharePoint.Client;
</span></span></pre><h2>Option #2: Using the built-in web services directly</h2>First step is to ad a reference to the Lists web service. This service is located at <siteUrl>/_vti_bin/Lists.asmx. I have called the reference ListsService.<br />
<a href="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjDcYyYelt7AIBU_l1R5GQWpfZqKNERXKxTPJYPT3cTdFXRr9vZoNMx7xO11oiNknE2TFErGSaeMTYEzwVp_l3-Qg2VhyfuPctzKnKms9g0HuBWOIU6O-mP0fQARS-_d3cbuPNg_cfHsSo3/s1600-h/AddListsWebService%25255B5%25255D.png"><img alt="AddListsWebService" border="0" height="464" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEh_Iwjc6DWJNR4dGYO5zDaOE5PXlclngecZb4QghIz3xC_VUppE-9v2Xi6c_dbAZAgxJGwJ22YC662R07dO5FbMhzPPMgc1uMbj9UuvxZ940atLSJdDt3NYFVAR-3QeKu7qD2wHlry-R3av/?imgmax=800" style="background-image: none; border-bottom-width: 0px; border-left-width: 0px; border-right-width: 0px; border-top-width: 0px; display: inline; padding-left: 0px; padding-right: 0px; padding-top: 0px;" title="AddListsWebService" width="608" /></a><br />
<em>Tip: If you cannot add a web reference in Visual Studio, start by adding a service reference, then in the wizard click ‘Advanced’, and then ‘Web service’.</em><br />
This web service includes a method to get the attachments for a specific list item. Use it like this:<br />
<pre style="background: white; line-height: normal;"><span style="font-family: Consolas;"><span style="color: blue;">public</span><span style="color: black;"> </span><span style="color: #2b91af;">IEnumerable</span><span style="color: black;"><</span><span style="color: blue;">string</span></span><span style="font-family: Consolas;"><span style="color: black;">> GetAttachmentUrls(
</span><span style="color: blue;">string</span></span><span style="font-family: Consolas;"><span style="color: black;"> siteUrl,
</span><span style="color: #2b91af;">ICredentials</span></span><span style="font-family: Consolas;"><span style="color: black;"> credentials,
</span><span style="color: #2b91af;">Guid</span></span><span style="font-family: Consolas;"><span style="color: black;"> listId,
</span><span style="color: blue;">int</span></span><span style="font-family: Consolas;"><span style="color: black;"> itemId)
{
</span><span style="color: #2b91af;">List</span><span style="color: black;"><</span><span style="color: blue;">string</span><span style="color: black;">> urls = </span><span style="color: blue;">new</span><span style="color: black;"> </span><span style="color: #2b91af;">List</span><span style="color: black;"><</span><span style="color: blue;">string</span></span><span style="font-family: Consolas;"><span style="color: black;">>();
</span><span style="color: #2b91af;">Lists</span><span style="color: black;"> service = </span><span style="color: blue;">new</span><span style="color: black;"> </span><span style="color: #2b91af;">Lists</span></span><span style="font-family: Consolas;"><span style="color: black;">();
service.Url = siteUrl.TrimEnd(</span><span style="color: #a31515;">'/'</span><span style="color: black;">) + </span><span style="color: #a31515;">"/_vti_bin/Lists.asmx"</span></span><span style="font-family: Consolas;"><span style="color: black;">;
service.Credentials = credentials;
</span><span style="color: #2b91af;">XmlNode</span></span><span style="font-family: Consolas;"><span style="color: black;"> node = service.GetAttachmentCollection(
listId.ToString(</span><span style="color: #a31515;">"B"</span></span><span style="font-family: Consolas;"><span style="color: black;">),
itemId.ToString());
</span><span style="color: blue;">foreach</span><span style="color: black;"> (</span><span style="color: #2b91af;">XmlNode</span><span style="color: black;"> childNode </span><span style="color: blue;">in</span></span><span style="font-family: Consolas;"><span style="color: black;"> node.ChildNodes)
{
urls.Add(childNode.InnerText);
}
</span><span style="color: blue;">return</span><span style="color: black;"> urls;
}
</span></span></pre>The URLs you get by this method are absolute URLs, not server-relative URLs. Note that if you already have a ClientContext object, you can use this to get the service URL and credentials, thus making the signature of the two methods identical.<br />
To make it work add these references:<br />
<pre style="background: white; line-height: normal;"><span style="color: blue; font-family: Consolas;">using</span><span style="font-family: Consolas;"><span style="color: black;"> SPCodeCollection.Client.ListsService;</span></span><span style="background-color: white; font-family: Consolas;"> *</span>
<span style="font-family: Consolas;"><span style="color: blue;">using</span></span><span style="font-family: Consolas;"><span style="color: black;"> System.Xml;
</span><span style="color: blue;">using</span><span style="color: black;"> System.Net;
</span></span></pre>* = namespace of your web service reference.<br />
<h2>Summing it up, and then some</h2>We can get the item attachments solely by using the SharePoint Client Object Model. This requires several web service calls (through the ClientContext.ExecuteQuery method). If performance is of essence, a better method may be to call the Lists.GetAttachmentCollection method.<br />
Apart from the performance, the difference between the two methods is that the client object model way provides you with server-relative URLs that you can use diectly to download the files with through the client object model. The web service method gives you absolute urls instead. To convert the absolute URLs to server-relative URLs you could do the following:<br />
<pre style="background: white; line-height: normal;"><span style="font-family: Consolas;"><span style="color: blue;">string</span><span style="color: black;"> serverRelativeUrl = url.Substring(url.IndexOf(ctx.Web.ServerRelativeUrl));
</span></span></pre>where ctx is our client context. This if course requires that the web relative URL has been loaded into context which, in the worst scenario, requires an extra web service call.Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com3tag:blogger.com,1999:blog-1353604901404890269.post-8436809654936848742012-03-17T07:19:00.001+01:002012-03-17T07:22:46.645+01:00SharePoint Sandbox: How to specify the solution ID<div class="my-summary"> <p>When creating solutions for the SharePoint sandbox, whether it is for SharePoint 2010 or Office365, the solution ID must be given whenever some kind of code-behind is used. This is because SharePoint needs to know which assembly to use. Without the solution ID SharePoint would just look in the GAC for the assembly, which would produce a 'File could not be found' exception.</p> <p>I will show how to supply the solution ID for a number of SharePoint components, and also describe a work-around in a case where we cannot specify a solution ID.</p></div> <a name='more'></a> <h4>Features with feature receivers</h4> <p>The solution ID is inserted the same place as the definition of receiver-assembly and -class in the Feature-tag in the featrure.xml file:<br><Feature ReceiverAssembly="..." ReceiverClass="..." SolutionId="MySolutionID"><br> ...<br></Feature></p> <h4>Content types with event receivers</h4> <p>The solution ID is inserted as an extra SolutionID-tag in the receiver definition in the content type element manifest:<br><Elements xmlns="<a href="http://schemas.microsoft.com/sharepoint/"">http://schemas.microsoft.com/sharepoint/"</a>><br> <ContentType ...><br> <XmlDocuments><br> <XmlDocument NamespaceURI="<a href="http://schemas.microsoft.com/sharepoint/events"">http://schemas.microsoft.com/sharepoint/events"</a>><br> <spe:Receivers xmlns:spe="<a href="http://schemas.microsoft.com/sharepoint/events"">http://schemas.microsoft.com/sharepoint/events"</a>><br> <spe:Receiver><br> ...<br> <spe:Assembly>...</spe:Assembly><br> <spe:Class>...</spe:Class><br> <spe:SolutionId>MySolutionID</spe:SolutionId><br> </spe:Receiver><br> </spe:Receivers><br> </XmlDocument><br> </XmlDocuments><br> </ContentType><br></Elements></p> <h4>Web parts</h4> <p>The web part definition does not need any solution ID defined. But when adding the web part to a page through a module, the solution ID must be given as part of the inserted web part script in the module element manifest:<br><Elements xmlns="<a href="http://schemas.microsoft.com/sharepoint/"">http://schemas.microsoft.com/sharepoint/"</a>><br> <Module ...><br> <File ...><br> <AllUsersWebPart ...><br> <![CDATA[<br> <webParts><br> <webPart xmlns="<a href="http://schemas.microsoft.com/WebPart/v3"">http://schemas.microsoft.com/WebPart/v3"</a>><br> <metaData><br> ...<br> <Solution SolutionId="MySolutionID" xmlns="<a href="http://schemas.microsoft.com/sharepoint/"">http://schemas.microsoft.com/sharepoint/"</a> /><br> </metaData><br> </webPart><br> </webParts><br> ]]><br> </AllUsersWebPart><br> </File><br> </Module><br></Elements></p> <h4>Custom page</h4> <p>Unfortunately you are not able to supply a solution ID reference in the Page directive on a custom page. Doing so will result in a syntax error. If you need to deploy your own pages to the sandbox, you need to use a different approach.</p> <p>If you need to add a solution reference to your page it is because you need some code-behind to run. If you don’t need code-behind then you don’t ned to reference the solution ID.</p> <p>So since we cannot add a solution ID to the page reference without ruining the page, we cannot make the page inherit from a custom page class. Instead we use a built-in page, and stick a custom web part on it (which, as you can see above <strong>does</strong> allow for a solution ID.</p> <p>If you don’t like the web part zones on the built-in pages there is nothing preventing you from defining your own. Just copy for example the STS default.aspx, and then change the zones. You could for example have a single large zone in which you place your web part. The point is that you ned to inherit from a page that is defined in the GAC, so with a sandboxed solution the page should inherit from for example <strong>Microsoft.SharePoint.WebPartPages.WebPartPage</strong>.</p> <p>Since we don’t have the option to create a layouts page in the sandbox the above would be the way to create a sandboxed settings page.</p> <h4>Summary</h4> <p>In the above you can find examples for the SharePoint sandbox on how to specify the solution ID for:</p> <ul> <li>Feature receivers</li> <li>Event receivers</li> <li>Web parts</li></ul> <p>Also, I explin how to create something that looks like a custom page with code-behind.</p> Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-47454939077021996112011-11-14T13:46:00.001+01:002012-01-12T22:48:58.419+01:00How to update the assembly in sandboxed solutions<div class="my-summary">
<p>
Sometimes we need to make changes to the assembly in a deployed SharePoint 2010 sandboxed solution. Perhaps we need to add something in an event receiver. Or perhaps we added a bug fix (not relevant - the code always works). Or some custom webpart needs to do something else than what it is doing right now.</p>
<p>
The only problem is that the solution is deployed in a production environment. We can't just delete and recreate the site, then add the updated solution package, and then tell the users to start all over. So what do we do?
</p>
<p>
Not to worry: This is a piece of cake - and a small piece at that.
</p>
</div>
<a name='more'></a>
<p class="my-sub-heading">
Why cant we just swap the solution package for the updated version?
</p>
<p>
You cannot just remove the original solution package and then deploy the updated package on the site instead. When you deactivate a package you also remove the feature definitions. When you then activate the updated solution the site collection features are automatically activated. This may cause conflicts with already-created content. Also, you might need to activate some web-scoped features (because of some feature dependencies) that could cause even worse conflicts:
<ul>
<li>Your feature could make configuration changes to the site</li>
<li>... or add content ...</li>
<li>... or interfer with changes made by the administrator ...</li>
<li>... or (you get the picture).</li>
</ul>
</p>
<p class="my-sub-heading">
So what do we do instead?
</p>
<p>
Instead of deactivating, deleting, uploading and activating, you just need to upgrade the solution package. For a 'I only changed the code' upgrade this is really simple:
</p>
<ol>
<li>Make the code changes in your project. Don't change the assembly version - this will lead to problems with particularly web part references.</li>
<li>Build the solution and create the WSP file. Don't change the solution ID - this must be the same as in the original solution for SharePoint to be able to match the old and the new solution.</li>
<li><span class="my-emphasize">Rename the WSP file.</span> A good idea would be to include the version in the file name. Minor versions (X.Y) could designate code-only changes, while major versions (X.0) could designate schema changes, added functionality etc..</li>
<li><span class="my-emphasize">Upload the renamed WSP file.</span> Instead of a 'Activate' option you will be presented with a 'Upgrade' option. Use it.</li>
</ol>
<p>
Or the short version: Change the code. Rename the WSP. Upload, and press 'Upgrade'.
</p>
<p>
If you followed the simple steps above (the last two being the crucial ones) then the only effect is that you have swapped the original assembly for your updated assembly. No other changes were made.
</p>
<p>
Of course if you need to make further changes, such as adding fields, new lists etc., then you have do dive into the new feature versioning features (eh... functionality). The <a class="my-code-inline" href="http://msdn.microsoft.com/en-us/library/ff595308.aspx">UpgradeActions Element</a> is what you need to look at then.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com1tag:blogger.com,1999:blog-1353604901404890269.post-28745735068485617002011-11-08T06:45:00.002+01:002011-11-23T15:16:41.361+01:00How to upload and download documents using the client object model<div class="my-summary">
<p>
With the increasing popularity of SharePoint 2010, and especially after the introduction of Office365, the market for client applications for SharePoint is increasing.
In response, Microsoft has released the SharePoint Client Object Model, which is a wrapper library for the SharePoint web services.
</p>
<p>
In this post I will demonstrate how you can use the Client Object Model (dare we use the abbreviation COM?) to upload and download files to and from SharePoint.
This may prove a central operation if you are migrating an existing SharePoint site to Office365. Other scenarios include uploading an existing file store to document libraries in a local SharePoint farm,
or creating plug-in's to existing applications that need to process your documents.
</p>
</div>
<a name='more'></a>
<p class="my-sub-heading">The client context</p>
<p>
The central class for the client code is the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.client.clientcontext.aspx" class="my-code-inline">ClientContext</a>. An instance of this class defines the connection to SharePoint, and any request, whether for getting or setting data, passes through the <span class="my-code-inline">ClientContext</span> instance.
The general procedure is as folows:
<ol>
<li>Define the requests you need to make using objects from the Client Object Model.</li>
<li>Use the client context instance to execute the requests and load the data from SharePoint.</li>
<li>Perform any processing of the data on the client side.</li>
<li>(Optional) Use the client context instance to re-submit the data into SharePoint.</li>
</ol>
You can reuse the client context instance for multiple requests.
</p>
<p class="my-sub-heading">Downloading documents</p>
<p>
To download documents you need to know the URL to the document. This is stored in the document list item as <span class="my-code-inline">item["FileRef"]</span>, or you may just happen to know what it is.
No matter how we get it, we need the server-relative URL to the document. Once we have that, we can download the document with just a few lines of client code:
</p>
<div class="my-code">
string fileRef = item["FileRef"].ToString();<br />
string fileName = fileRef.Substring(fileRef.LastIndexOf('/') + 1);<br />
using (FileInformation fileInfo = File.OpenBinaryDirect(myContext, fileRef))<br />
{<br />
using (IO.FileStream writeStream = IO.File.Open(@"c:\MyFolder\" + fileName, IO.FileMode.Create))<br />
{<br />
fileInfo.Stream.CopyTo(writeStream);<br />
writeStream.Flush();<br />
}<br />
}
</div>
<p>
Of course you can name the document whatever you want. The above is just a simple way of getting the original file name.
</p>
<p class="my-sub-heading">Uploading documents</p>
<p>
Uploading the document is not much more difficult than downloading it. In general, when we create things with the client object model, we need to make some kind of creation information object.
For documents (and files in general) the class to use is the <a class="my-code-inline" href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.client.filecreationinformation.aspx">FileCreationInformation</a> class.
</p>
<div class="my-code">
FileCreationInformation myCreateInfo = <br />
new FileCreationInformation();<br />
createInfo.Content = System.IO.File.ReadAllBytes(@"c:\MyFolder\MyDocument.docx");<br />
createInfo.Url = "MyDocument.docx";<br />
File myFile = myLibrary.RootFolder.Files.Add(myCreateInfo);<br />
myContext.Load(myFile);<br />
myContext.ExecuteQuery();
</div>
<p>
This code will upload the document to the root folder of the document library. If you are not satisfied with this, you can find the desired folder by iterating through the folders and sub-folders using <span class="my-code-inline">myLibrary.RootFolder.Folders</span>.
</p>
<p class="my-sub-heading">Setting document metadata</p>
<p>
Often we need to set some metadata on the documents. This can be achieved by using the list item connected to the file. Continuing from the code in the previous section:
</p>
<div class="my-code">
ListItem myItem = myFile.ListItemAllFields;<br />
myItem["myField"] = "myFieldvalue"]<br />
//... set additional fields<br />
<br />
myItem.Update();<br />
myContext.ExecuteQuery();
</div>
<p>
You can add whatever fields you like. Just be sure to use the internal name of the field.
</p>
<p class="my-sub-heading">Summing it up</p>
<p>
With the code above we can download files - documents, images, or anything - from SharePoint libraries.
We can also upload files, and set their metadata.
</p>
<p>
The code works against any version of SharePoint, even SharePoint Online (as part of Office365). The last, however, requires the use of Live-ID when login in. In an upcoming post I will describe how to do this.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com2tag:blogger.com,1999:blog-1353604901404890269.post-52801517481416217432011-10-30T08:15:00.001+01:002011-10-31T06:25:56.019+01:00SPAudit: How to read from the audit log<div class="my-summary">
<p>
As part of my series on SharePoint auditing, I will demonstrate how to read from the audit log. This is useful if you are restricted to Windows SharePoint Service or SharePoint Foundation, where the out-of-the-box reports are not available, or just need to make your own custom reports.
<p>
The auditing is centered around the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spaudit.aspx" class="my-code-inline">SPAudit</a> class, which is available in all versions of SharePoint 2007 and SharePoint 2010, except in sandboxed solutions.
</p>
</div>
<a name='more'></a>
<p class="my-sub-heading">How do you read the audit log?</p>
<p>
SharePoint has a number of built-in autid log reports. These are made available when activating the <span class="my-code-inline">Reporting feature</span>, and thus cannot be used in Windows SharePoint Service (WSS) and SharePoint Foundation. Once enabled, the audit reports are accessible from the link 'Audit log reposts' in the 'Site Collection Settings' column. Any audit events you create yourself may be viewed through the 'Run a custom report' link. Either way, you are presented with an Excel spreadsheet with the audit data. This may or may not be what you were looking for.
</p>
<p>
There are several scenarios where you could benefit from reading directly from the audit log. Some of these include:
<ul>
<li>You are restricted to WSS or SharePoint Foundation. In this case you do not have any GUI for the audit log, but the functionality is still there ready to use.</li>
<li>You record custom events to the audit log, and wish to process these at a later time, for example in a timer job.</li>
<li>You are not satisfied with the out-of-the-box reports, and wish to display your own selection of events.</li>
</ul>
</p>
<p>
The two central classes when reading from the audit log are the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.aspx" class="my-code-inline">SPAuditQuery</a> and the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.aspx" class="my-code-inline">SPAuditEntry</a> classes.
</p>
<p class="my-sub-heading">
Reading the audit log with SPAuditQuery
</p>
<p>
If you need to get the entries from the audit log, then <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.aspx" class="my-code-inline">SPAuditQuery</a> is the class to use. The audit log exists on a site collection basis, and therefore it makes sense to instantiate the SPAuditQuery with a site collection, for example by using the current context:
</p>
<div class="my-code">
SPAuditQuery query = <br/> new SPAuditQuery(SPContext.Current.Site);
</div>
<p>
This of course would not work in a timer job - here you have to get your site collection object in some other way.
</p>
<p>
Next step is to set up filters for our query. We generally don't want to get all the entries in the audit log at once. We have several means of adding filters to the query:
<ul>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.restricttolist.aspx" class="my-code-inline">RestrictToList</a>: Restricts the query to events relevant to the specified list.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.restricttolistitem.aspx" class="my-code-inline">RestrictToListItem</a>: Restricts the query to events that are relevant to the specified list item.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.restricttouser.aspx" class="my-code-inline">RestrictToUser</a>: Restricts the query to events relevant to the specified user.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.rowlimit.aspx" class="my-code-inline">RowLimit</a>: Restricts the query to a maximum number of returned entries. <span class="my-highlight">Warning:</span> If using SharePoint 2007 please read the section <a href="#snakeinthegarden">A snake in the garden</a> below.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.setrangestart.aspx" class="my-code-inline">SetRangeStart</a>: Restricts the query to entries on or after a specified time.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.setrangeend.aspx" class="my-code-inline">SetRangeEnd</a>: Restricts the query to entries on or before a specified time.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditquery.addeventrestriction.aspx" class="my-code-inline">AddEventRestriction</a>: Restricts the query to the specified type of event (using the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditeventtype.aspx" class="my-code-inline">SPAuditEventType</a> enum). This may be done multiple times to query many event types at the same time. If this method is not called, the query will return entries for all event types.</li>
</ul>
</p>
<p>
Once you have created your audit query, the next thing is to run it to get the audit entries. This is done in the following way:
</p>
<div class="my-code">
SPAuditEntryCollection entries =
SPContext.Current.Site.Audit.GetEntries(query);
</div>
<p class="my-sub-heading">
Understanding the audit log entries
</p>
<p>
Once you have executed your query, you end up with a <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentrycollection.aspx">SPAuditEntryCollection</a>, which of course is a collection of <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.aspx">SPAuditEntry</a> instances. Each of these instances holds information of a specific audit event, which are defined through the properties of the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.aspx">SPAuditEntry</a> instance. The MSDN article describes these in detail, but I would like to highlight a few of them:
<ul>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.event.aspx">Event</a>: What actually happened? Was a field deleted? A member added to a group? What? This is exposed through a value of the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditeventtype.aspx">SPAuditEventType</a> enumeration.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.eventname.aspx">EventName</a>: A string that gives the name of the event - but only when the event type is SPAuditEventType.Custom. So this is how you distinguish your own events.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.itemtype.aspx">ItemType</a>: Which type of object did the event occur on? A list? A list item? A content type? This property used the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spaudititemtype.aspx">SPAuditItemType</a> enumeration.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.itemid.aspx">ItemId</a>: This specifies the exact object the event occurred on. The ID (which is a <a href="http://msdn.microsoft.com/en-us/library/system.guid.aspx">Guid</a>) is unique across the whole site collection. But if the event has recorded a deletion you will not be able to retrieve extra information on the object (because - you know - it has been <i>deleted</i>).</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.occurred.aspx">Occurred</a>: When did the event occur.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.userid.aspx">UserId</a>: The integer ID of the user who caused the event. The actual user may be retrieved by <span class="my-code-inline">SPWeb.SiteUsers.GetByID(userId)</span>. Note that the user may have been deleted in the mean time.</li>
<li><a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.eventdata.aspx">EventData</a>: A detailed review of what happened. Which group was the user added to? What field was updated? Which list was deleted? The event data is presented as XML. The <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spauditentry.eventdata.aspx">MSDN article</a> has an overview of the XML for the different event types. If you create you own event types, you would do well in using the same XML tags (groupid for the group ID, user for the uer ID etc.) as used for the standard events. This is just a recommendation that will make it easier for you to parse the XML, not a requirement. You can even ignore the XML format, and just write a string with a friendly description of the event. Just remember that you generally don't need to store information that is already present in the content database. For example, you would not need to store a user's name, email, phone number etc. in the audit log, since you can get them easily just by having the user ID.</li>
</ul>
</p>
<p class="my-sub-heading"><a name="snakeinthegarden">A snake in the garden</a></p>
<p>
There is a snake in the garden of SharePoint 2007: The property <span class="my-code-inline">RowLimit</span> was added to the <span class="my-code-inline">SPAuditQuery</span> class somewhere between Service Pack 1 and 2. When developing a solution for SharePoint 2007 we either have to require that Service Pack 2 is installed, or handle the <span class="my-code-inline">RowLimit</span> property with care.
</p>
<p>
The following code - which uses the System.Reflection namespace - takes care of the problem: Using reflection, we first check if the property exists, before we set it.
</p>
<div class="my-code">
private void SetRowLimit(SPAuditQuery query)<br/>
{<br/>
Type type = query.GetType();<br/>
PropertyInfo propInfo = type.GetProperty("RowLimit");<br/>
if (propInfo != null)<br/>
{<br/>
propInfo.SetValue(query, myRowLimit, null);<br/>
}<br/>
}
</div>
<p class="my-sub-heading">
Try it youself
</p>
<p>
If you have read my other auditing posts <a href="http://spuser.blogspot.com/2011/10/as-part-of-my-series-on-sharepoint.html">How to turn auditing on programmatically</a> and <a href="http://spuser.blogspot.com/2011/10/spaudit-how-to-write-to-audit-log.html">How to write to the audit log</a> there should be nothing stopping you from starting using the SharePoint audit log already today. Have fun.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com5tag:blogger.com,1999:blog-1353604901404890269.post-65862728913667538632011-10-14T11:37:00.004+02:002011-10-31T06:25:42.740+01:00SPAudit: How to write to the audit log<div class="my-summary">
<p>
As part of my series on SharePoint auditing, I will demonstrate how to write to the audit log. I will also make a few suggestions for what you can use custom auditing for.
</p>
<p>
The auditing is centered around the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spaudit.aspx" class="my-code-inline">SPAudit</a> class, which is available in all versions of SharePoint 2007 and SharePoint 2010, except in sandboxed solutions.
</p>
</div>
<a name='more'></a>
<p class="my-sub-heading">How do you write to the audit log?</p>
<p>
Writing to the audit log is quite easy. All you have to do is add the following line (in this case creating an <span class="ms-code-inline">Update</span> entry):
</p>
<div class="my-code">
audit.WriteAuditEvent(<br/>
SPAuditEventType.Update, <br/>
myEventSource, <br/>
myEventData);
</div>
<p>
Or, if you want to add a custom event - which would probably be the typical case:
</p>
<div class="my-code">
audit.WriteAuditEvent(<br/>
myEventName, <br/>
myEventSource, <br/>
myEventData);
</div>
<p>
This last overload results in an audit log entry of event type <span class="my-code-inline">SPAuditEventType.Custom</span>, and with event name <span class="my-code-inline">myEventName</span>.
</p>
<p>
The event data can be any string value you choose. When writing standard events SharePoint uses XML to describe the event. If you choose to do likewise, it will be a good idea to use some of the same tags
that SharePoint employs, such as <span class="my-code-inline">groupid</span> (a group ID) and <span class="my-code-inline">user</span> (a user ID). This will make it easier for you if you want to later display the event data in a readable fashion.
</p>
<p class="my-sub-heading">What can you use the audit log for?</p>
<p>
There are several uses for the audit log:
</p>
<ul>
<li>
You can record events in your custom solutions. Later, you can retrieve these events for additional processing. For example, you might have a list of products, the price of which are to be updated in a BI database at regular intervals. An event handler on the list registers changes in prices in the audit log, while ignoring other changes to the products. Then, once a week, a timerjob retrieves these event from the audit log, and updates the prices for the selected products in the BI database.
</li>
<li>
As a variation of this, you can use the audit log for error logging. Instead of writing to the ULS log - which only records the error on the local server - you can write to the audit log, and display errors farm-wide on an application page.
</li>
<li>
You can use the audit log as an alternative to the workflow history list. SharePoint by default removes entries from these lists when they are 6 month old, which could cause a loss of important data.
</li>
</ul>
<p class="my-sub-heading">
Try it youself
</p>
<p>
If you have read my other auditing posts <a href="http://spuser.blogspot.com/2011/10/as-part-of-my-series-on-sharepoint.html">How to turn auditing on programmatically</a> and <a href="http://spuser.blogspot.com/2011/10/spaudit-how-to-write-to-audit-log.html">How to write to the audit log</a> there should be nothing stopping you from starting using the SharePoint audit log already today. Have fun.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com1tag:blogger.com,1999:blog-1353604901404890269.post-39876201566245602232011-10-12T11:05:00.006+02:002011-11-25T07:15:57.310+01:00SPAudit: How to turn auditing on programmatically<div class="my-summary">
<p>
As part of my series on SharePoint auditing, I will demonstrate how to enable auditing programmatically. I will also point out a single dragon lurking in the background.
</p>
<p>
The auditing is centered around the <a href="http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spaudit.aspx" class="my-code-inline">SPAudit</a> class, which is available in all versions of SharePoint 2007 and SharePoint 2010, except in sandboxed solutions.
</p>
</div>
<a name='more'></a>
<p class="my-sub-heading">How do you turn the auditing on?</p>
When we turn auditing on, what we are really doing is enabling some event handlers in SharePoint. Some of these events we can already catch using SharePoint event receivers, others can not be caught that way. But the core functionality of having an audit log, and being able to CRUD it (well, CRD, since we can Create, Read and Delete, but not Update) is always available.
</p>
<p>
To turn on auditing you would use the following code, adding whatever flags you need:
</p>
<div class="my-code">
SPAudit audit = siteCollection.Audit;<br/>
audit.AuditFlags = <br/>
SPAuditMaskType.SecurityChange | <br/>
SPAuditMaskType.SchemaChange;<br/>
audit.Update();
</div>
<p>
This of course removes whatever audit settings there were on the site collection previously. If you want to just ensure that some specific audit types are turned on, then use:
</p>
<div class="my-code">
SPAudit audit = siteCollection.Audit;<br/>
audit.AuditFlags = <br/>
siteCollection.Audit.AuditFlags | <br/>
SPAuditMaskType.SecurityChange | <br/>
SPAuditMaskType.SchemaChange;<br/>
audit.Update();
</div>
<p>
Not only the site collection has audit settings. A <span class="my-code-inline">SPAudit</span> instance is available from instances of the following classes: <span class="my-code-inline">SPSite</span>, <span class="my-code-inline">SPWeb</span>, <span class="my-code-inline">SPList</span>, <span class="my-code-inline">SPFolder</span> and <span class="my-code-inline">SPListItem</span>.
</p>
<p class="my-sub-heading">Here be dragons...</p>
<p>
If you turn on the auditing for all events, then beware: Your content database will swell with audit data. This is particularly true if you use <span class="my-code-inline">SPAuditMaskType.View</span>. Subhajit Chatterjee has <a href="http://blogs.msdn.com/b/subhajitc/archive/2009/05/20/tip-the-untold-story-of-audit-logs-in-sharepoint.aspx">a post about the problem</a>. Basically, his advise is to have a plan for archiving the audit data somewhere else.
</p>
<p>
The Masked SharePointer describes <a href="http://themaskedsharepointer.blogspot.com/2009/03/setting-up-timer-job-to-cleanup-audit.html">a timerjob for cleaning up the audit log periodically</a>.
</p>
<p class="my-sub-heading">
Try it youself
</p>
<p>
If you have read my other auditing posts <a href="http://spuser.blogspot.com/2011/10/spaudit-how-to-write-to-audit-log.html">How to write to the audit log</a> and <a href="http://spuser.blogspot.com/2011/10/spaudit-how-to-read-from-audit-log.html">How to read from the audit log</a> there should be nothing stopping you from starting using the SharePoint audit log already today. Have fun.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-851471693358825982011-09-09T22:52:00.001+02:002011-09-09T23:02:14.982+02:00How to get language-specific field names in SharePoint 2010<div class="my-summary">
<p>
In SharePoint 2010 you can access a site using different languages. Recently I had to synchronize list data with an external system, using the list field display names. Since the display name changed with the language, I had to make sure that the external system got the data in a specific language.
</p>
</div>
<a name='more'></a>
<p>
It is in fact quite easy to get the display name of the fields in a specific language, regardless of which language the current user has elected. All it really takes is the following line:
</p>
<div class="my-code">string languageSpecificDisplayName =
myField.TitleResource.GetValueForUICulture(new CultureInfo(myLCID))
</div><br/>
In the above code I just had to give a specific language identifier (the <span class="my-inline-code">myLCID</span>), and SharePoint returned the display name in the language I needed.<br/>
The language identifier could be hard-coded, or stored in a property bag, a list, or perhaps in the external system.Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-17786878470229584622011-05-25T06:50:00.002+02:002011-11-07T06:26:38.754+01:00Powershell Caching<div class="my-summary">
<p>
After having spent some time activating SharePoint features on a test environment using Powershell, and scratching my head wondering why my code doesn't work, I have come to the following conclusion: Powershell is caching me.
</p>
</div>
<a name='more'></a>
<p>
What I experienced was things like a feature that could not be activated by powershell. However, when activating that same feature through the Sharepoint user interface, everything went according to plan.
Another problem was a resource file that I updated, but where the updates did not show up when I used powershell, only when activating features manually in the browser.
</p>
<p>
After some experimenting (and screaming) I came to the trembling conclusion:
</p>
<p class="my-emphasize">
Powershell is caching my files!
</p>
<p>
I still don't quite understand how it can do it, but when I activate a feature through powershell session for the first time, the intended assembly is used. If I then update the assembly and try again in the same powershell session, then the original assembly is used again, and my updates are ignored. Now, if powershell used the assembly directly I would just slap my forehead and exclaim "of course!". But as I understand it powershell just tells SharePoint to do something, and SharePoint then does it. How can powershell force SharePoint to use a cached assembly?
</p>
<p>
Well, the solution is so simple I can't believe it took me so long to find:
</p>
<p class="my-emphasize">
USE A NEW POWERSHELL SESSION!
</p>
<p>
When the laughing has subsided, I might update this post to outline what my powershell script does, and how.
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-73759535583091228442011-05-06T21:03:00.000+02:002011-05-19T05:51:56.031+02:00How to see the exception call stack in SharePoint 2010<div class="my-summary">SharePoint by default hides any erors that occur, instead displaying a friendly error message. However, during the development process, these error messages are rather unfriendly, since they prevent us from seeing what really goes on.<br />
</div><a name='more'></a><br />
In SharePoint 2007 we can disable SharePoint's default error habit by making the following changes to the <span class="my-code-inline">web.config</span> file in the root folder of the web application:<br />
<br />
<div class="my-code"><?xml version="1.0" encoding="UTF-8" standalone="yes"?><br />
<configuration><br />
<SharePoint><br />
<SafeMode CallStack="true"> <span class="my-highlight">⇐ Set CallStack to true</span><br />
...<br />
</SafeMode><br />
</SharePoint><br />
...<br />
<system.web><br />
<customErrors mode="Off" /> <span class="my-highlight">⇐ Set mode to Off</span><br />
<compilation debug="true"> <span class="my-highlight">⇐ Set debug to true</span><br />
...<br />
</system.web><br />
...<br />
</configuration></div>This will display exception messages and callstacks in a SharePoint 2007 environment. Try the same changes in SharePoint 2010, and you will still not see the exception.<br />
<br />
Dispair not, having done the above you are more than halfway there. What you need to do now is to go to the <span class="my-code-inline">14\TEMPLATE\LAYOUTS</span> folder and locate the <span class="my-code-inline">web.config</span> file here. Then make the following changes:<br />
<br />
<div class="my-code"><?xml version="1.0" encoding="UTF-8" standalone="yes"?><br />
<configuration><br />
<system.web><br />
<compilation debug="true"/> <span class="my-highlight">⇐ Set debug to true</span><br />
<customErrors mode="Off" /> <span class="my-highlight">⇐ Set mode to Off</span><br />
...<br />
</system.web><br />
...<br />
</configuration></div>This will show you the errors you are making in SharePoint 2010.Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-66774388498685370102011-04-10T09:12:00.000+02:002011-05-10T06:52:08.891+02:00How to easily implement SharePoint custom list forms<div class="my-summary"><p>In a recent project I had to create a number of custom list forms. As it turns out, custom list forms are quite easy to create, requiring only an extra attribute in the list schema.xml, a template file, and a custom control to actually render the custom form.<br />
</p></div><a name='more'></a><br />
<p>Basically, I needed to make a list where some fields were editable for some users at some stage in a process, while other fields were editable for other users at another stage.<br />
</p><p>Imagine a design company, where a designer is responsible for adding new ideas to a product list. The product manager then calculates production cost, after which the sales manager adds an expected retail price, and determines whether to send the idea into production. Each of these persons adds information to the same list item, but only to fields within their own area of expertice. Further more, the sales manager may not add the sales information until the product manager has completed his work.<br />
</p><p class="my-heading">How to register a custom form for a list template<br />
</p><p>First, we need to register the custom form in the list definition. In the schema.xml of the list template, typically at the end, there is a <span class="my-code-inline"><Forms></span> tag. To register the custom form simply add a <span class="my-code-inline">Template</span> attribute to the <span class="my-code-inline"><Form></span> elements.<br />
</p><p>In the scenario described above I need to implement some logic in the New and Edit form. For the Display form the SharePoint standard form is sufficient.<br />
Therefore I add a template attribute to two of the forms. As it is the same logic I need to implement for the two forms I just use the same template.<br />
</p><div class="my-code"><Forms><br />
<Form<br />
Type="DisplayForm"<br />
Url="DispForm.aspx"<br />
SetupPath"pages\form.aspx"<br />
WebPartZoneID="Main" /><br />
<Form<br />
Type="EditForm"<br />
Url="EditForm.aspx"<br />
SetupPath"pages\form.aspx"<br />
Template="MyCustomForm"<br />
WebPartZoneID="Main" /><br />
<Form<br />
Type="NewForm"<br />
Url="NewForm.aspx"<br />
SetupPath"pages\form.aspx"<br />
Template="MyCustomForm"<br />
WebPartZoneID="Main" /><br />
</Forms><br />
</div><p class="my-heading">How to create the custom form<br />
</p><p>The form template is created using a template in the SharePoint CONTROLTEMPLATES folder. Start by creating an .ascx file in the folder, naming it whatever you like. Then open the DefaultTemplates.ascx file in the same folder, and copy all the top declarations (before the first <span class="my-code-inline"><SharePoint:RenderingTemplate></span> tag) to your file. Also copy the template called 'ListForm' from DefaultTemplates.ascx and insert it in your file. Set the ID of the RenderingTemplate to the name you entered in the template attribute above (MyCustomForm in my example).<br />
</p><p><span class="my-emphasize">WARNING:</span> Visual Studio by default adds an ID attribute to all server controls. You need to remove all ID's in your template that were not present in the original 'ListForm' template, as they will break the form. Go to Tools > Options > Text Editor > HTML > Miscellaneous in Visual Studio if you want to change this default behaviour.<br />
</p><div class="my-code"><%@ Control Language="C#" AutoEventWireup="false" %><br />
<%@Assembly Name="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %><br />
<%@Register TagPrefix="SharePoint" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.WebControls"%><br />
<%@Register TagPrefix="SPHttpUtility" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" namespace="Microsoft.SharePoint.Utilities"%><br />
<%@ Register TagPrefix="wssuc" TagName="ToolBar" src="~/_controltemplates/ToolBar.ascx" %><br />
<%@ Register TagPrefix="wssuc" TagName="ToolBarButton" src="~/_controltemplates/ToolBarButton.ascx" %><br />
<SharePoint:RenderingTemplate ID="MyCustomForm" runat="server"><br />
<Template><br />
<SPAN id='part1'><br />
<SharePoint:InformationBar runat="server"/><br />
<wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbltop" RightButtonSeparator=" " runat="server"><br />
<Template_RightButtons><br />
<SharePoint:NextPageButton runat="server"/><br />
<SharePoint:SaveButton runat="server"/><br />
<SharePoint:GoBackButton runat="server"/><br />
</Template_RightButtons><br />
</wssuc:ToolBar><br />
<SharePoint:FormToolBar runat="server"/><br />
<TABLE class="ms-formtable" style="margin-top: 8px;" border=0 cellpadding=0 cellspacing=0 width=100%><br />
<SharePoint:ChangeContentType runat="server"/><br />
<SharePoint:FolderFormFields runat="server"/><br />
<SharePoint:ListFieldIterator runat="server"/><br />
<SharePoint:ApprovalStatus runat="server"/><br />
<SharePoint:FormComponent TemplateName="AttachmentRows" runat="server"/><br />
</TABLE><br />
<table cellpadding=0 cellspacing=0 width=100%><tr><td class="ms-formline"><IMG SRC="/_layouts/images/blank.gif" width=1 height=1 alt=""></td></tr></table><br />
<TABLE cellpadding=0 cellspacing=0 width=100% style="padding-top: 7px"><tr><td width=100%><br />
<SharePoint:ItemHiddenVersion runat="server"/><br />
<SharePoint:ParentInformationField runat="server"/><br />
<SharePoint:InitContentType runat="server"/><br />
<wssuc:ToolBar CssClass="ms-formtoolbar" id="toolBarTbl" RightButtonSeparator=" " runat="server"><br />
<Template_Buttons><br />
<SharePoint:CreatedModifiedInfo runat="server"/><br />
</Template_Buttons><br />
<Template_RightButtons><br />
<SharePoint:SaveButton runat="server"/><br />
<SharePoint:GoBackButton runat="server"/><br />
</Template_RightButtons><br />
</wssuc:ToolBar><br />
</td></tr></TABLE><br />
</SPAN><br />
<SharePoint:AttachmentUpload runat="server"/><br />
</Template><br />
</SharePoint:RenderingTemplate><br />
</div><p>If you now deploy the list template you will see what looks exactly like the standard SharePoint forms. Fair enough - we haven't made any changes to the template yet. But although you can't differentiate them, it will actually be your custom forms that are displayed.<br />
</p><p class="my-heading">How to render som fields as read-only and others as editable<br />
</p><p>The next step is to implement the logic for our custom form. We replace the control that renders the fields, the <span class="my-code-inline">SharePoint:ListFieldIterator</span>, with a custom control. Remember to add a <span class="my-code-inline">@Register</span> tag to the template file to register the custom control. In this example I implement it as a server control (no .ascx file) but you could just as well choose a user control (with .ascx file). In that case it must also be included in the CONTROLTEMPLATES folder.<br />
</p><div class="my-code"><%@ Control Language="C#" AutoEventWireup="false" %><br />
...<br />
<%@ Register TagPrefix="MyPrefix" Assembly="MyAssembly 4 part name" Namespace="MyNamespace" %><br />
...<br />
<SharePoint:RenderingTemplate ID="MyCustomForm" runat="server"><br />
...<br />
<SharePoint:FolderFormFields runat="server"/><br />
<!--<SharePoint:ListFieldIterator runat="server"/>--><br />
<MyPrefix:MyFieldIterator ID="MyControl" runat="server /><br />
<SharePoint:ApprovalStatus runat="server"/><br />
...<br />
</SharePoint:RenderingTemplate><br />
</div><p>Now, we want to control which fields are shown as read-only, as editable, or as hidden altogether. What we don't want to do is reinvent the rendering of the different SharePoint field types. Therefore we use the <span class="my-code-inline">ListFieldIterator</span> control to render the fields. This is what SharePoint does, so the fields will be rendered in the proper SharePoint way.<br />
</p><p>Two things to know about the <span class="my-code-inline">ListFieldIterator</span>:<br />
</p><ul><li>The iterator will by default display all the fields that the user should see, according to the field definitions in the list. This can be overridden by setting the ExcludeFields property to the internal name(s) of the field(s) that should not be displayed (separate multiple names with ';#').</li>
<li>The iterator will determine the rendering mode (display, new or edit) from the context. This can be overridden by setting the ControlMode property.</li>
</ul><p>What we are going to do is create a <span class="my-code-inline">ListFieldIterator</span> instance for each user group. Each instance excludes the fields that the group does not 'own'. We then set the <span class="my-code-inline">ControlMode</span> of each instance according to which group the current user belongs to.<br />
</p><div class="my-code">using System.Web.UI;<br />
using Microsoft.SharePoint;<br />
using Microsoft.SharePoint.WebControls;<br />
<br />
namespace MyNamespace<br />
{<br />
/// <summary><br />
/// My implementation of the field rendering control.<br />
/// </summary><br />
public class MyFieldIterator : UserControl<br />
{<br />
/// <summary><br />
/// Creates the ListFieldIterator instances and sets their properties.<br />
/// </summary><br />
protected override void CreateChildControls()<br />
{<br />
// Create ListFieldIterator instances,<br />
// and set their ExcludeFields.<br />
var designerIterator = new ListFieldIterator()<br />
{<br />
ControlMode = SPControlMode.Display,<br />
ExcludeFields = "ProductionCost;#RetailPrice;#ToProduction"<br />
};<br />
var productManagerIterator = new ListFieldIterator()<br />
{<br />
ControlMode = SPControlMode.Display,<br />
ExcludeFields = "Title;#Description;#RetailPrice;#ToProduction"<br />
};<br />
var salesManagerIterator = new ListFieldIterator()<br />
{<br />
ControlMode = SPControlMode.Display,<br />
ExcludeFields = "Title;#Description;#ProductionCost"<br />
};<br />
<br />
// Set ControlMode depending on the current user.<br />
var groups = SPContext.Current.Web.SiteGroups;<br />
var currentMode = SPContext.Current.FormContext.FormMode;<br />
if (groups["My Designers"].ContainsCurrentUser)<br />
{<br />
designerIterator.ControlMode = currentMode;<br />
}<br />
if (groups["My Product Managers"].ContainsCurrentUser)<br />
{<br />
productManagerIterator.ControlMode = currentMode;<br />
}<br />
if (groups["My Sales Managers"].ContainsCurrentUser)<br />
{<br />
salesManagerIterator.ControlMode = currentMode;<br />
}<br />
<br />
// Add controls.<br />
this.Controls.Add(designerIterator);<br />
this.Controls.Add(productManagerIterator);<br />
this.Controls.Add(salesManagerIterator);<br />
<br />
base.CreateChildControls();<br />
}<br />
}<br />
}<br />
</div><p>Deploy to the global assembly cache, login as member of one of the three groups, and add, edit and view items in the list.<br />
</p><p class="my-heading">More advanced forms<br />
</p><p>The above form is a simple custom form. For more advanced scenarios you might want to handle the 'save'-action differently - setting extra fields, performing advanced validation, or creating related items. This could be done using an event receiver, but you could also create your own control that inherits from <span class="my-code-inline">SharePoint:SaveButton</span>.<br />
</p><p>An even more advanced form might render the fields in an altogether different way, drawing on data from external sources, or handling multiple list items simoultaneously. In such scenarios you would probably replace everything inside the <span class="my-code-inline">SharePoint:RenderingTemplate</span> with custom controls, including toolbars, information on created/modified by, approval state and more.<br />
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com5tag:blogger.com,1999:blog-1353604901404890269.post-56329652643622343972011-03-30T21:53:00.000+02:002011-04-02T22:59:26.766+02:00How to programmatically get content approval information on a list item<div class="my-summary"><p>Today, as I was working on a custom list form for SharePoint, I needed to get some information on versioning and content approval for the items in the list. It turned out to be quite straight-forward. Here is what I did.<br />
</p></div><a name='more'></a><br />
<p class="my-heading">Getting the versions<br />
</p><p>The versioning information is accessed through the SPListItem.Versions property. This will give you access to the SPListItemVersionCollection, which contains all the versions <span class="my-emphasize">that the current user has access to</span>. Notice the emphasize. If you access the versions with elevated privileges you will see all versions, including drafts.<br />
</p><p>A user with read-only privileges will se only approved versions.<br />
</p><p>The versions collection is ordered with the latest version <span class="my-emphasize">first</span>. This means that if you do the following:<br />
</p><div class="my-code">foreach (SPListItemVersion myVersion in myListItem.Versions)<br />
{<br />
Console.WriteLine(myVersion.VersionLabel);<br />
}</div><p>you will get a result similar to this (in this case three versions have been made).<br />
</p><div class="my-output">3.0<br />
2.0<br />
1.0<br />
</div><p>To get the field values for a given version you access them through an indexer on the version. As index you use either an integer, or the internal name of the field. The version also has a Fields collection, which you can use in the following manner:<br />
</p><div class="my-code">foreach (SPField field in myVersion.Fields)<br />
{<br />
Console.WriteLine(<br />
"Field {0}: {1}",<br />
field.InternalName,<br />
myVersion[field.InternalName]);<br />
}<br />
</div><p>This seems somewhat counter-intuitive to me - I would have expected the field values to be accessed by <span class="my-code-inline">myVersion.Fields[guid or internal name]</span>.<br />
</p><p class="my-heading">Getting the approval information<br />
</p><p>Approved versions are basically like all other versions: The approval information is stored in the version fields. Of special interest for approval is the field <span class="my-code-inline">_ModerationStatus</span>:</p><div class="my-code"><ul><li>0 equals Approved</li>
<li>1 equals Rejected</li>
<li>2 equals Pending</li>
</ul></div><p>By combining this with other information in the version you could get the approved versions and the approval information by doing the following:<br />
</p><div class="my-code">foreach (SPListItemVersion myVersion in myListItem.Versions)<br />
{<br />
if (myVersion["_ModerationStatus"].ToString() == "0")<br />
{<br />
Console.WriteLine(<br />
"Date: {0} Ver.: {1} Approver: {2} Comments: {3}",<br />
myVersion.Created,<br />
myVersion.VersionLabel,<br />
myVersion.CreatedBy.LookupValue,<br />
myVersion["_ModerationComments"]);<br />
}<br />
}<br />
</div><p>which in my example yields the following output:<br />
</p><div class="my-output">Date: 30-03-2011 19:06:46 Ver.: 4.0 Approver: MyDomain\tmj Comments: My last comment.<br />
Date: 30-03-2011 19:06:03 Ver.: 3.0 Approver: MyDomain\tmj Comments: My next comment.<br />
Date: 30-03-2011 19:05:17 Ver.: 1.0 Approver: MyDomain\tmj Comments: My first comment.<br />
</div><p>In the above we can see that I did not approve version 2.0, therefore it is not shown in the output.<br />
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-23016998400029142012011-03-26T08:19:00.000+01:002011-05-01T09:03:17.985+02:00Getting the publishing permission levels<div class="my-summary"><p>SharePoint has a small number of built-in permission levels, or role definitions as they are called code-wise. Several of these are easy to access through the SPRoleType enum. <br />
</p><p>However, the publishing features create new permission levels that are not found in SPRoleType. Although they can be accessed using code such as <span class="my-code-inline">web.RoleDefinitions["Approve"]</span>, this requires hardcoded strings, and is not practical in a localized setting. Luckily, there is a way to access the localization of these permission levels. I got the solution from <a href="http://sadomovalex.blogspot.com/2011/02/get-manage-hierarchy-and-approve-role.html">Alexey Sadomov</a>, and present it here in a slightly modified form.<br />
</p></div><a name='more'></a><br />
<p>The localization resources used for the publishing permission levels are embedded in the publishing assembly. Furthermore, they are internal to the assembly, meaning that we cannot with ordinary means access them. Therefore we resort to use <span class="my-code-inline">Reflection</span>.<br />
</p><p>Just as with an ordinary resource file, the publishing resources are accessed through a string key. A complete list of these are found <a href="http://msdn.microsoft.com/library/microsoft.sharepoint.publishing.internal.stringids_members(v=office.12).aspx">here</a>.<br />
To make the reflection part easy, we create a wrapper class that handles the reflection, and adds a public method that accepts a key and a language ID, and returns the localized string. Here is the wrapper class in all it's simplicity:<br />
</p><div class="my-code">using System.Globalization;<br />
using System.Reflection;<br />
using System.Resources;<br />
public static class PublishingResources<br />
{<br />
private static ResourceManager manager;<br />
static PublishingResources()<br />
{<br />
var assembly = Assembly.Load("Microsoft.SharePoint.Publishing.Intl, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");<br />
PublishingResources.manager = new ResourceManager(<br />
"Microsoft.SharePoint.Publishing.Strings", <br />
assembly);<br />
}<br />
public static string GetString(string key, uint lcid)<br />
{<br />
return PublishingResources.Manager.GetString(<br />
key,<br />
new CultureInfo((int)lcid));<br />
}<br />
}<br />
</div><p>Now you can access the publishing resources in your SharePoint code using the language of the current web:<br />
</p><div class="my-code">PublishingResources.GetString(key, web.Language);<br />
</div><p>To access the 'Approve' and 'Manage Hierarchy' permission level names use the keys 'RoleNameApprover' and 'RoleNameHierarchyManager'. <span class="my-emphasize">Note:</span> To use the above with SharePoint 2010 simply change the assembly version to 14.0.0.0.<br />
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-38793998486472280922011-03-23T21:44:00.000+01:002011-05-01T09:02:43.535+02:00Working with custom permission levels in SharePoint<div class="my-summary"><p>Creating custom permission levels in SharePoint is not hard. You can do it through the GUI, by navigating to the<span class="my-code-inline">_layouts/addrole.aspx</span> page on any site.<br />
But you might want to do it through code instead, deploying the new permission level with a feature and assigning it to groups and users. This also includes more options for defining the permission level than the GUI does. Below I show how to define your own permission levels.<br />
</p><p>Should you want to see what permissions are used for the standard permission levels, then <a href="http://blogs.msdn.com/b/sridhara/archive/2008/06/27/what-permissions-is-behind-the-permission-levels-roles-in-sharepoint.aspx">this link</a> is a good place to look.<br />
</p></div><a name='more'></a><br />
<p>Imagine that we need a permission level that lets the user do all the things they normally do through the 'Contribute' permission level, EXCEPT deleting list items.<br />
</p><p>We could solve this by using the <span class="my-code-inline">ItemDeleting</span> event receiver, but that is not a user-friendly way - first giving them the option to delete an item (through the ECB menu), then telling them that they can't do it anyway. Instead, we create and deploy a custom permission level, after which SharePoint will ensure that users are only presented with the actions they are allowed to perform.<br />
</p><p class="my-heading">Create a custom permission level<br />
</p>First we create a feature, and add a feature receiver.<br />
<div class="my-code"><?xml version="1.0" encoding="utf-8"?><br />
<Feature<br />
Id="{MyGuid}"<br />
Title="My feature"<br />
Scope="Web"<br />
ReceiverAssembly="My four-part assembly name"<br />
ReceiverClass="MyNamespace.MyFeatureReceiver"<br />
xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<ElementManifests><br />
</ElementManifests><br />
</Feature><br />
</div><p>Then, in the feature receiver, we define the new permission level. By the way, code-wise it is called a SPRoleDefinition. We need to define which permissions the permission level should include. In our example we will base it on the permissions for the <span class="my-code-inline">Contribute</span> permission level, but remove the permissions to delete.<br />
</p><div class="my-code">public override void FeatureActivated(SPFeatureReceiverProperties properties)<br />
{<br />
var web = (SPWeb)properties.Feature.Parent;<br />
var contribRoleDef = web.RoleDefinitions.GetByType(SPRoleType.Contributor);<br />
var newRoleDef = new SPRoleDefinition();<br />
newRoleDef.Name = "My permission level";<br />
newRoleDef.Description = "Description of my permission level.";<br />
newRoleDef.BasePermissions = contribRoleDef<br />
^ SPBasePermissions.DeleteListItems<br />
^ SPBasePermissions.DeleteVersions;<br />
web.RoleDefinitions.Add(newRoleDef);<br />
}<br />
</div><p>Note that it is not necessary to call web.Update().<br />
</p><p class="my-heading">Assign the custom permission level to a member<br />
</p><p>To assign the custom permission level to a member (user or group), do the following (in a feature receiver, or whereever you want to run the code):<br />
</p><div class="my-code">var assignment = new SPRoleAssignment(myMember);<br />
assignment.RoleDefinitionBindings.Add(web.RoleDefinitions["My permission level"]);<br />
web.RoleAssignments.Add(assignment);<br />
web.Update();<br />
</div><p>Have a nice permission.<br />
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-81739477739398280292011-03-19T07:56:00.001+01:002011-09-16T23:37:19.113+02:00How to add images to picture library with thumbnails using CAML<div class="my-summary">Adding images to a SharePoint picture library is done basically like adding any other type of file to a library in SharePoint: You create a module, point it towards the library, and includes the files. Well, there is a catch...<br />
</div><a name='more'></a><br />
<p>Imagine you have a feature that creates a picture library. The same feature also contains a folder ('<span class="my-code-inline">Files</span>') with image files to add. Then you would write something like this in your <span class="my-code-inline">elements.xml</span>:<br />
</p><div class="my-code"><?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Module<br />
Name="Images"<br />
Url="MyPictures"<br />
Path="Files"><br />
<File <br />
Url="MyImage.png" <br />
Path="MyImage.png" <br />
Type="GhostableInLibrary"/><br />
</Module><br />
</Elements><br />
</div><p>Well and done: Your images are added to the picture library. But, wait, where did my thumbnails go?<br />
</p><p class="my-heading">Adding thumbnails through CAML<br />
</p><p>SharePoint displays two different thumbnails for each image:<br />
</p><ul><li>On the picture library '<span class="my-code-inline">All items</span>' > '<span class="my-code-inline">Thumbnails</span>' view, which is the default view for the picture library. This is the 'w' thumbnail.</li>
<li>On the picture display form. This is the 't' thumbnail.</li>
</ul><p>To deploy an image with thumbnails you must also supply the two thumbnails mentioned above. What you do is this:<br />
</p><ol><li>Start by deploying your image to some test site.</li>
<li>Go to the picture library, locate your image, then check it out, and check it in.</li>
<li>SharePoint will now have created the thumbnails for you. Download a copy of the two thumbnails (right-click the thumbnail images and save as...). Keep the names SharePoint created.</li>
<li>Now include these two images in your feature, by adding them to subfolders called '<span class="my-code-inline">_t</span>' and '<span class="my-code-inline">_w</span>'. These folders will be hidden by SharePoint, so the user won't see them.</li>
</ol><div class="my-code"><?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Module<br />
Name="Images"<br />
Url="MyImages"<br />
Path="Images"><br />
<File<br />
Url="MyImage.jpg"<br />
Path="MyImage.jpg" <br />
Type="GhostableInLibrary"/><br />
<File <br />
Url="_t/MyImage_jpg.jpg" <br />
Path="_t\MyImage_jpg.jpg" <br />
Type="GhostableInLibrary"/><br />
<File <br />
Url="_w/MyImage_jpg.jpg" <br />
Path="_w\MyImage_jpg.jpg" <br />
Type="GhostableInLibrary"/><br />
</Module><br />
</Elements><br />
</div><p>This approach will deploy the image and display thumbnails right away. However, it does not work on SharePoint 2010 for the w thumbnail.<br />
</p>
<p class="my-heading">Adding thumbnails through code<br />
</p><p>If the above seems too cumbersome, or you just have a large amount of pictures, then you could force SharePoint to create the thumbnails, by effectively doing what you do manually above: Checking the image out and in. This could be done in a feature receiver.<br />
</p><p>First the CAML:<br />
</p><div class="my-code"><?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Module<br />
Name="Images"<br />
Url="MyImages"<br />
Path="Images"><br />
<File<br />
Url="MyImage.jpg"<br />
Path="MyImage.jpg"<br />
Type="GhostableInLibrary"/><br />
</Module><br />
</Elements><br />
</div><p>Then the feature receiver:<br />
</p><div class="my-code">public override void FeatureActivated(SPFeatureReceiverProperties properties)<br />
{<br />
var web = (SPWeb)properties.Feature.Parent;<br />
var image = web.GetFile("MyImages/MyImage.jpg");<br />
image.CheckOut();<br />
image.CheckIn("Thumbnails created", SPCheckinType.OverwriteCheckIn);<br />
}<br />
</div><p>The feature receiver approach is in my opinion the 'correct' way to do it, since we are asking SharePoint to do whatever image processing it wants. By deploying the thumbnails using CAML (the first approach) you could say that we are 'tricking' SharePoint into displaying the thumbnails - there may be a registration of some kind we are missing, which could explain why it is not working in SharePoint 2010...<br />
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com0tag:blogger.com,1999:blog-1353604901404890269.post-49468521085423692982011-03-13T21:49:00.002+01:002011-10-17T06:34:07.893+02:00How to create lookup fields in CAML that works<div class="my-summary">
<p>
For my first post on this blog I will demonstrate how to avoid the most common problem with deploying SharePoint lookup fields.
</p>
<p>
Basically, a lookup field is defined using CAML just like any other SharePoint field. However, there is a problem: You need to add a reference to the lookup list. There are several ways to achieve this. Below, I have outlined three standard ways to do this, and a fourth way, my preferred, that handles the problems inherent in each of the other three solutions.
</p>
</div>
<a name='more'></a>
<p class="my-heading">
First method: CAML using the list URL
</p>
<p>
A standard way to configure the lookup field with CAML is to insert the lookup list URL:
</p>
<div class="my-code">
<?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Field<br />
ID="{68D0555E-0A41-4CBB-BEFF-663FCD0B1DBC}"<br />
DisplayName="My Lookup Field"<br />
Name="MyLookupField"<br />
Group="My Fields"<br />
Type="Lookup"<br />
List="Lists/MyLookupList"<br />
ShowField="Title"/><br />
</Elements>
</div>
<p>
This is fine, as long as you only want to use the lookup field on a single web. If you are creating a lookup field that will be used across an entire site collection, this approach will not work, since the field will always try to reference a list on the local web, where that list may not exist.
</p>
<p class="my-heading">
Second method: CAML using list and web IDs
</p>
<p>
Another way to achieve the same result is to use the lookup list ID:
</p>
<div class="my-code">
<?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Field<br />
ID="{68D0555E-0A41-4CBB-BEFF-663FCD0B1DBC}"<br />
Name="MyLookupField"<br />
DisplayName="My Lookup Field"<br />
Group="My Fields"<br />
Type="Lookup"<br />
List="{5421D53F-8B62-4618-B3C3-1A331D4024CF}"<br />
ShowField="Title"/><br />
</Elements>
</div>
<p>
The problem here is that we do not necessarily know the list ID when the CAML for the field is written - for example if the site the field and list exist on has not yet been created.
</p>
<p class="my-heading">
Third method: Programmatically
</p>
<p>
A third way to create the lookup field is purely programmatically, typically in a feature receiver.
</p>
<div class="my-code">
web.Fields.AddLookup(<br />
strDisplayName,<br />
lookupListId,<br />
lookupWebId,<br />
bRequired);<br />
web.Update();
</div>
<p>
This requires you to also add the field programmatically to lists, views and content types, with the added challenge of rearranging the fields to get the desired order.
</p>
<p class="my-heading">
Fourth method: CAML and feature receiver
</p>
<p>
My preferred way to handle lookup fields combines the best parts of method 2 and 3 above. It goes like this: Create a lookup field in CAML, using an 'empty' ID for the list, then update this value, and the field WebId, programmatically in the feature receiver:
</p>
<p>
CAML:
</p>
<div class="my-code">
<?xml version="1.0" encoding="utf-8" ?><br />
<Elements xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<Field<br />
ID="{68D0555E-0A41-4CBB-BEFF-663FCD0B1DBC}"<br />
Name="MyLookupField"<br />
DisplayName="My Lookup Field"<br />
Group="My Fields"<br />
Type="Lookup"<br />
List="{00000000-0000-0000-0000-000000000000}"<br />
ShowField="Title"/><br />
</Elements>
</div>
<p>
Feature Receiver:
</p>
<div class="my-code">
public override void FeatureActivated(<br />
SPFeatureReceiverProperties properties)<br />
{<br />
var web = ((SPSite)properties.Feature.Parent).RootWeb;<br />
var field = (SPFieldLookup)web<br />
.Fields<br />
.GetFieldByInternalName("MyLookupField");<br />
var list = web.Lists["MyLookupList"];<br />
field.SchemaXml = field.SchemaXml.Replace(<br />
"List=\"{00000000-0000-0000-0000-000000000000}\"",<br />
string.Format(<br />
"List=\"{0}\"",<br />
list.ID.ToString("B")));<br />
field.LookupWebId = list.ParentWeb.ID;<br />
field.Update(true);<br />
}
</div>
<p>
The advantage of this is that you will use the correct lookup list and web IDs, without knowing these beforehand. And, since the field is defined in CAML, you may include it straightaway in list templates and content types, eliminating the need to programmatically rearrange field order:
</p>
<p>
List Template (schema.xml):
</p>
<div class="my-code">
<?xml version="1.0" encoding="utf-8"?><br />
<List <br />
xmlns:ows="Microsoft SharePoint"<br />
Title="My List"<br />
Url="Lists/MyList"<br />
BaseType="0"><br />
<MetaData><br />
<Fields><br />
<Field<br />
ID="{68D0555E-0A41-4CBB-BEFF-663FCD0B1DBC}"<br />
Name="MyLookupField"<br />
Type="Lookup"/><br />
</Fields><br />
<Views><br />
...<br />
</Views><br />
<Forms><br />
...<br />
</Forms><br />
</MetaData><br />
</List>
</div>
<p>
Content Type:
</p>
<div class="my-code">
<?xml version="1.0" encoding="utf-8" ?><br />
<Elements<br />
xmlns="http://schemas.microsoft.com/sharepoint/"><br />
<ContentType<br />
ID="0x010012BCCE07B75A42b6896061693D823F0B"<br />
Name="My Content Type"<br />
Group="My Content Types"><br />
<FieldRefs><br />
<FieldRef<br />
ID="{68D0555E-0A41-4CBB-BEFF-663FCD0B1DBC}"<br />
Name="MyLookupField"/><br />
</FieldRefs><br />
</ContentType><br />
</Elements>
</div>
<p>
This approach ensures that the lookup field will always target the correct list, no matter what web it is used on (inside the same site collection, that is).
</p>Thomas Møller Jørgensenhttp://www.blogger.com/profile/11456520016777810170noreply@blogger.com4