Cliff Green's Blog

  • SharePoint Console Application Permissions for Read Only Access

    I was recently challenged with trying to come up with a least privilege scenario for running a console application or windows service against a SharePoint Farm to perform read-only type operations.  Without the correct permissions there are several exceptions you will run into, including File IO, UnauthorizedAccess, SPException, etc.  A joy to wade through.  :-) 

    I used this KB article as a starting point for what we knew would work.  It states that the identity used to run the console application should be the following:

    • Farm Administrator
    • Read/Write database permissions on the configuration and content databases.
    • Site collection administrator
    • Permissions to each site in the site collection

    I started my quest with no permissions granted on the farm and slowly elevated them until I was able to successfully iterate the object model.  I ended up with close to what is published above but I was showing DBO permissions were required for the database.  After consulting with some people internally I was able to use the SQL Role WSS_Content_Application_Pools for the configuration database, but was still requiring DBO for the content databases.  Again, some help internally brought to my attention that a stored procedure was required to enumerate the sites that exist in the site collection.  I was able to grant execute permissions for the user on the content databases using the following command:

    GRANT EXECUTE TO [DOMAIN\username]

    I also changed the permissions for the user on the content database to db_datareader.  After the changes were made the console application was able to successfully iterate the content in the site collection in a read-only fashion.  The final permissions were as follows:

    • Farm Administrator
    • db_datareader permissions on the content databases
    • WSS_Content_Application_Pools permissions on the SharePoint_Config (default name) database
    • GRANT EXECUTE permission on the stored procs in the content database
    • Site collection administrator
    • Permissions to each site in the site collection

    This could be further reduced to the exact stored procs required in the content databases, but I’ll leave that for another day.  Write permissions don’t seem to be required on the content database as indicated in the KB and we can leverage the SharePoint specific SQL role for the configuration database.  The fact that we can reduce this to db_datareader instead of read / write on the content databases should make some of the DBAs of the world a little happier.

  • Reporting Services: Value Either too Large or too Small for a Decimal

    I have seen a few reports recently that have been upgraded from SSRS 2005 to SSRS 2008 where a ‘Value either too Large or too small for a decimal’ appears in the chart under certain circumstances.  This typically manifest itself when an expression in the data series for the chart looks something like that below:

    =IIf(Fields!ISMONEYAMOUNTSW.Value = 0 and Fields!TIMEINSECONDS.Value > 0 , 
        Sum(Fields!TIMEINSECONDS.Value), 
        Iif(Fields!ISMONEYAMOUNTSW.Value = 1 and Fields!TOTALAMOUNT.Value > 0,
            SUM(Fields!TOTALAMOUNT.Value),""))

    Notice the empty string in the last expression as part of the FalsePart of the IIf expression.  This also happens if ‘Nothing’ is used.  These values, “” or ‘Nothing’, don’t evaluate to decimals in this case and the rendering engine has trouble converting them.  In the previous version I imagine this was ignored.  In SQL Reporting Services 2008 the value attempts to convert to the decimal type unsuccessfully.  The fix is simply to change the expression to a ‘0’ so that it is evaluated properly as shown below.

    =Iif(Fields!ISMONEYAMOUNTSW.Value = 0 And Fields!TIMEINSECONDS.Value > 0 , 
        Sum(Fields!TIMEINSECONDS.Value), 
        Iif(Fields!ISMONEYAMOUNTSW.Value = 1 And Fields!TOTALAMOUNT.Value > 0,
            SUM(Fields!TOTALAMOUNT.Value),0))
  • SharePoint CopyIntoItems and Setting a Document Content Type

    I can’t take credit or this one, but a colleague I have been working with was able to figure this out.  He doesn’t blog, so I am sharing it with the greater community.  Our quest was to upload a document into a document library using the Copy.asmx web service and set the content type on the document as well as other field information as part of the upload.  The method takes a FieldInformation array, so at first glance this seemed like it was going to be pretty easy.  We all know what happens next.

    Initial attempts were successful at uploading, but not setting the content type.  The content type would always end up as Document, the default.  We tried several different variations, but nothing worked.  There are also others that seem to have had the same fate and come up with other workarounds.

    My colleague had the idea to call the GetItem() method for an item that had the content type already set, and there was the answer.  The type of field for a content type is FieldType.Choice, not FieldType.Text, which was the assumption all along.  Calling GetItem() returned the following:

    <FieldInformation Type="Choice" DisplayName="Content Type" InternalName="ContentType" Value="***"/>

    From there it was straightforward.  Uploading and setting the content type in one simple method call.  He asked that I give his company a plug, so … those people at SAS are pretty smart aren’t they. :-)

  • Reporting Services Migration Error: Object reference not set to an instance of an object.

    In working with a customer doing a migration from Reporting Services 2005 to 2008 an error appeared when in preview mode that didn’t appear in the previous version.  The error was simply #Error in the report and the Error pane in Visual Studio showed The Value expression for the field ‘<my field name>’ contains an error: Object reference not set to an instance of an object.  We have all seen the ‘object reference not set’ at one point in our lives.  After doing some trouble shooting it turns out that the report was calling out to an external assembly to do some work building a data table and then getting the summary values for the data table in another call.  The data table was originally built using a hidden table in the report and the values were accessed in a field in another report table.  Because of the positioning of the tables the hidden table would execute first, therefore creating the data table in code, and the expression to get the values would execute afterwards.

    This reliance of execution order was the root cause of the problem.  In SSRS 2008 the execution order has changed.  Refer to Robert Bruckner’s blog for more information.  For this particular report the data table was not created first which caused the row collecting summary data from the expected table to be null.  This in turn caused the value being retrieved to be null.  Although the code checked for (x == DbNull.Value) it didn’t explicitly check for (x == null), so a cast later on in the code block created the ‘object reference not set to an instance of an object’ error.

    What can cause confusion is the error itself, which says that Value expression for the field <y> contains an error.  I racked my brain trying to figure out why that particular field was causing the error.  It wasn’t.  The logic, to my best guess, just parses the last parameter calling out to the external assembly that is a field in the report.  I took my field that was stated to be the issue and changed it to an empty string.  The next run of the report showed that the next parameter was the issue, and the next and the next until I had eliminated all of them.

    The important thing to remember here is that relying on execution order in a report may cause issues when migrating to SSRS 2008.  Try to avoid it if at all possible.

  • SharePoint Tools Released

    I work in the ISV Practice here at Microsoft.  There are two tools recently released to the SharePoint community that should have ISVs and Enterprise customers excited. 

    SPDisposeCheck provides a static source code analysis tool to check for leaks that may be caused by not disposing of SPSite and SPWeb objects properly.  While most people that have worked with SharePoint know of these perils, this tool can provide a second set of eyes on your code.  For those new to the product, SPDisposeCheck will allow you to uncover those instances where improperly disposed objects would result in leaked memory.  Instances where this occurs isn’t always obvious, so the tool can help reduce leaks that make it to production.

    Another exciting tool is the SPDiag tool.  This tool greatly simplifies gathering data required for troubleshooting issues in a SharePoint environment.  When working on issues or performance problems getting a good snapshot if the farm environment can help uncover the root cause of the issues quickly and get customers back to work.

  • Discover SharePoint Context within an Integrated SSRS report

    With SQL Reporting Services integration with SharePoint there may be a need to be contextually aware of the site or list where your report resides.  With the ability to add code or reference custom assemblies in our report we have a starting point to gather this information and leverage information that may be available to us in a site.  The first thing I thought of when thinking through this issue was that I could leverage the SPContext object.  This would provide the ability to get the current site Url and reference information that way.  Unfortunately, the reference to SPContext within the report is null because it is being rendered in an IFRAME.  So we can't use that mechanism to do that.  So the next idea was to get a reference to HttpContext and leverage that in order to find out where we are in SharePoint.  When a report runs from a SharePoint document library it runs within the RSViewerPage.aspx page.  The actual Url request in my environment is, http://cliffgre-mossvm/SiteDirectory/reports/_layouts/ReportServer/RSViewerPage.aspx?RelativeReportUrl=/SiteDirectory/reports/RDL/TestReport.rdl&Source=http%3A%2F%2Fcliffgre%2Dmossvm%2FSiteDirectory%2Freports%2FRDL%2FForms%2FAllItems%2Easpx&DefaultItemOpen=0.  The report, however, is run using a pointer to the Report Server.  In my environment the Web Application lives in http://cliffgre-mossvm.  When I request a report it runs in a reference to http://cliffgre-mossvm:8000/ReportServer, so it runs completely out of the SharePoint environment.  The Url request in the report looks something like this:

    http://cliffgre-mossvm:8000/ReportServer/Reserved.ReportServer?http://cliffgre-mossvm/SiteDirectory/reports/RDL/TestReport.rdl
    &rs:SessionID=l1ahe1yas2icsrjuiar0na55&rs:command=Render&rs:Format=HTML4.0&rc:HTMLFragment=true&rc:Section=1
    &rc:StreamRoot=/SiteDirectory/reports/Reserved.ReportViewerWebPart.axd?ReportSession=l1ahe1yas2icsrjuiar0na55&
    ControlID=05e0aa50c74646f3858bf6847b08f5f2&Culture=1033&UICulture=1033&ReportStack=1&OpType=ReportImage&StreamID=
    &rc:ResourceStreamRoot=/SiteDirectory/reports/Reserved.ReportViewerWebPart.axd?ReportSession=l1ahe1yas2icsrjuiar0na55
    &ControlID=05e0aa50c74646f3858bf6847b08f5f2&Culture=1033&UICulture=1033&ReportStack=1&OpType=ReportImage&ResourceStreamID=
    &rc:ActionScript=ClientReport05e0aa50c74646f3858bf6847b08f5f2.ActionHandler&rc:StyleStream=true&rc:LinkTarget=_top
    &rc:UserAgent=Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+5.2;+.NET+CLR+1.1.4322;+.NET+CLR+2.0.50727;+.NET+CLR+3.0.04506.30;+InfoPath.2;+.NET+CLR+3.0.04506.648;+.NET+CLR+3.5.21022)
    &rc:Toolbar=false&rs:ErrorResponseAsXml=true&rs:AllowNewSessions=false

    One of the things that gets passed as part of the query string is the path to the report.  Notice the first line above after the question mark (?).  We can leverage that information and parse the Url in order to find out the site and list where the report lives.  So now we have the Url to the report and we can leverage the SharePoint object model to get a reference to SPSite, SPWeb and down to SPList.  Because we are running a custom assembly in reporting services it will have to be marked to AllowPartiallyTrustedCallers using the [assembly: AllowPartiallyTrustedCallers] attribute.

    If only things were that simple.  Once we add the code to our custom assembly and install it in the GAC we will get the error message below.

    SecurityException: Request for the permission of type 
    'Microsoft.SharePoint.Security.SharePointPermission, 
    Microsoft.SharePoint.Security, Version=12.0.0.0, Culture=neutral, 
    PublicKeyToken=71e9bce111e9429c' failed.
    

    In order to get around this problem we have to assert the required SharePoint permissions in order to have access to the SharePoint object model.  By surrounding our code with an Assert() for SharePointPermission our code will run and allow us to navigate the SharePoint object model as we see fit.  The full coding example is shown below.  Although it only simply returns the .PortalName and .Url properties, it can be extended to meet your needs.

       1: public string GetInfo() {
       2:     string siteInfo = "";
       3:     try {
       4:  
       5:         string siteCollectionUrl = "";
       6:         string queryString = HttpContext.Current.Request.Url.ToString();
       7:         int indexStart = (queryString.IndexOf("?")+1);
       8:         int indexEnd = queryString.IndexOf( "&" );
       9:         string reportUrl = queryString.Substring( indexStart, (indexEnd-indexStart) );
      10:         siteCollectionUrl = reportUrl.Substring( 0, reportUrl.LastIndexOf("/") );
      11:  
      12:         SharePointPermission sharepointPerm = new SharePointPermission( PermissionState.Unrestricted );
      13:         sharepointPerm.Assert();
      14:  
      15:         using( SPSite siteCollection = new SPSite( siteCollectionUrl ) ) {
      16:             siteInfo = siteCollection.PortalName + ": "+ siteCollection.Url;
      17:         }
      18:  
      19:         sharepointPerm.Deny();
      20:  
      21:     } catch( Exception ex ) {
      22:         siteInfo = ex.Message + ex.StackTrace;
      23:     }
      24:     return siteInfo;
      25: }

    Now that we have done this we have a report that can leverage the SharePoint object model as needed.  Of course this won't work within Visual Studio so the report has to be deployed to SharePoint in order for the code to run properly.

  • Controlling Page Size in a Reporting Services Report

    The default page size in the US is Letter, 8.5in x 11in.  In other parts of the world A4 (8.3in x 11.7in) is the standard.  Reports in Reporting Services are hard coded as part of the design process.  In order to control the page size in the rendering process we need to pass the proper Device Information Settings to the report at run time.  This will work for physical page oriented renders like PDF, Image, etc.  This is a simple Xml string that can be passed as a parameter to the report in order to control these settings.  Each export type has a different set of properties that can be overridden and controlled in this way.  The Device Info Settings for a PDF document can be found here.

    If we are using the Report Viewer control to display the reports we will need to disable the ability to print a report and potentially disable exporting the report, depending on the use cases we want to support.  To still provide this capability to our users we can expose the print and export properties using a link button or other postback capable control.  We can also expose the available print and export formats using a drop down or other mechanism.  Now when the user chooses to print or export the report we can override the process by passing the device info settings for the page height and width when the report is rendered.  In the example below we are exporting to PDF and passing the page height and width to match an A4 paper size as the targeted device (line 66).

       1: private void RenderReportToClient()
       2: {
       3:     //set credentials
       4:     RSExecuteProxy.ReportExecutionService rs = new RSExecuteProxy.ReportExecutionService();
       5:     rs.Credentials = System.Net.CredentialCache.DefaultCredentials;
       6:  
       7:     RSProxy.ReportingService2005 rsInfo = new RSProxy.ReportingService2005();
       8:     rsInfo.Credentials = System.Net.CredentialCache.DefaultCredentials;
       9:  
      10:     // init render args
      11:     byte[] result = null;
      12:     string reportPath = rptViewer.ServerReport.ReportPath;
      13:     string format = "PDF";
      14:     string historyId = null;
      15:  
      16:     string encoding;
      17:     string mimeType;
      18:     string extension;
      19:     RSExecuteProxy.Warning[] warnings = null;
      20:     string[] streamIDs = null;
      21:  
      22:     //init exec info
      23:     RSExecuteProxy.ExecutionInfo execInfo = new RSExecuteProxy.ExecutionInfo();
      24:     RSExecuteProxy.ExecutionHeader execHeader = new RSExecuteProxy.ExecutionHeader();
      25:  
      26:     rs.ExecutionHeaderValue = execHeader;
      27:  
      28:     //get report
      29:     execInfo = rs.LoadReport(reportPath, historyId);
      30:  
      31:     String SessionId = rs.ExecutionHeaderValue.ExecutionID;
      32:  
      33:     //get parameter info
      34:     ReportParameterInfoCollection parameters = rptViewer.ServerReport.GetParameters();
      35:  
      36:     //figure out how many parameters we will have 
      37:     //those with multi-value will need there own ParameterValue in the array
      38:     int paramCount = 0;
      39:  
      40:     foreach (ReportParameterInfo pramInfo in parameters)
      41:     {
      42:         paramCount += pramInfo.Values.Count;
      43:     }
      44:  
      45:  
      46:     RSExecuteProxy.ParameterValue[] prams = new SSRSWeb.RSExecuteProxy.ParameterValue[paramCount];
      47:  
      48:     int currentPramPosition = 0;
      49:  
      50:     //set pram values
      51:     foreach (ReportParameterInfo pramInfo in parameters)
      52:     {
      53:         foreach (string pramValue in pramInfo.Values)
      54:         {
      55:             prams[currentPramPosition] = new SSRSWeb.RSExecuteProxy.ParameterValue();
      56:             prams[currentPramPosition].Label = pramInfo.Name;
      57:             prams[currentPramPosition].Name = pramInfo.Name;
      58:             prams[currentPramPosition].Value = pramValue;
      59:             currentPramPosition++;
      60:         }
      61:     }
      62:  
      63:     rs.SetExecutionParameters(prams, "en-US");
      64:  
      65:     //build the device settings  (A4 8.3 × 11.7)
      66:     string deviceInfo = string.Format("<DeviceInfo><PageHeight>{0}</PageHeight><PageWidth>{1}</PageWidth></DeviceInfo>", "11.7in", "8.3in");
      67:  
      68:     //get report bytes
      69:     result = rs.Render(format, deviceInfo, out extension, out encoding, out mimeType, out warnings, out streamIDs);
      70:  
      71:     Response.ClearContent();
      72:     Response.AppendHeader("Content-Disposition", "inline;filename=report.pdf");
      73:     Response.AppendHeader("content-length", result.Length.ToString());
      74:     Response.ContentType = "application/pdf";
      75:     Response.BinaryWrite(result);
      76:     Response.Flush();
      77:     Response.Close();
      78: }

    When the report is saved you can view the PDF and examine the properties and notice that the page height and width is 8.3in x 11.7in as specified.

  • SharePoint Web Part Embedded Resource Permissions

    It is a normal expectation for companies to embed resources like javascript, CSS and other resources in an assembly.  Web Parts are no exceptions.  For those of you that deploy everything to the GAC, you can disregard the remainder of this post.  The rest of you, bear with me.  If we are not deploying to the GAC we are typically deploying to the bin.  The bin directory in SharePoint has the following permissions by default; full control for SYSTEM, Administrators and WSS_ADMIN_WPG groups, and read-only access for WSS_WPG group.  This is not sufficient for embedded resources to be loaded by accounts that don't exists in one of these groups.  If you run an assembly with embedded resources with anything other than an account that is in one of these groups you will get the following error:

    Exception raised when trying to access embedded resources:
    System.UnauthorizedAccessException: Access to the path 'C:\inetpub\wwwroot\wss\VirtualDirectories\80\bin\SampleWP.DLL' is denied. 
    at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath) 
    at System.IO.File.GetLastWriteTimeUtc(String path) at System.IO.File.GetLastWriteTime(String path) 
    at System.Web.Handlers.AssemblyResourceLoader.GetAssemblyInfoWithAssertInternal(Assembly assembly) 
    at System.Web.Handlers.AssemblyResourceLoader.GetAssemblyInfo(Assembly assembly) 
    at System.Web.Handlers.AssemblyResourceLoader.GetWebResourceUrlInternal(Assembly assembly, String resourceName, Boolean htmlEncoded) 
    at System.Web.Handlers.AssemblyResourceLoader.GetWebResourceUrl(Type type, String resourceName, Boolean htmlEncoded) 
    at System.Web.UI.ClientScriptManager.GetWebResourceUrl(Page owner, Type type, String resourceName, Boolean htmlEncoded) 
    at System.Web.UI.ClientScriptManager.RegisterClientScriptResource(Type type, String resourceName) 
    at TestWebPart.ResourcePermissionTest.OnPreRender(EventArgs e)
    

    The assembly doesn't have permission to access the bin directory.  If you don't get this error do an IIS reset and then access the web part as a user that isn't in one of these groups.  The difficult part of figuring this out was that if a user with permissions to write to the directory first accesses the web part it will render properly, not only for that user, but any user thereafter.  This will happen until the cache is exhausted.  So even during the debugging process I was wondering what the heck I just saw a couple of times.

    On the surface the easy solution is to change the permissions and set 'everyone' to have read / write access and move on.  What you will notice is that SharePoint will reset these permissions back to the standard after certain events.  One of those events is deploying a solution.  If you watch the directory you will notice that the bin permissions are recreated when this happens.  The bin directory is actually deleted and recreated.  So while the 'everyone' account will work temporarily, it isn't a long term solution.

    What about CAS?  Doesn't work.  Contrary to some of the other posts out there about this this isn't a CAS issue.  Granting the assembly 'Full' trust doesn't change the fact that an account that doesn't have access to the directory needs to write to it.  While you can set the web.config and disable impersonation or create a CAS policy that doesn't impersonate, this isn't the best solution in cases where you need to impersonate the user.  Windows Auth. comes to mind.  It would allow you to take the non-impersonated account and add them to one of the built-in groups so the web part would function properly.  This probably isn't an acceptable solution if you are an ISV or third party looking to distribute your web part to other parties.  They may not be open to adding least privilege accounts to groups on their servers

    One solution that does work is creating a folder in the web application directory and changing the web.config to add a probingPath in order to help the .NET runtime locate your assembly.  By creating your own directory you control the permissions and they aren't reset by SharePoint.  Your installation and setup are a little more trouble, but you have a solution that should work long term without sacrifices to the functionality of SharePoint or, most importantly, your web part.

  • Reporting Services: What Happens When I Upgrade

    Recently we were involved in a conversation around upgrading from SSRS 2005 to SSRS 2008.  The credit for this post goes to Robert Bruckner who guided us during the internal discussion.  When you move from SSRS 2005 to SSRS 2008 there are things that happen internally that may be unknown to you.   If you upload or deploy your SSRS 2005 reports to an SSRS 2008 server an attempt is made to upgrade the schema to SSRS 2008 RDL.  Should that upgrade fail, the report is flagged and an attempt to upgrade will not be made on that report again.  The report can still be run by a user, but it will leverage the 2005 processing engine.

    If the upgrade is successful we store the original report definition in the database so that you can get to the definition as it existed when deployed.  A call to the GetReportDefinition() method or clicking the 'edit' link in Report Manager will return the original RDL file.  The upgraded version of the report is stored as a compiled version as a 2008 report in the 2008 RDL.  This allows reporting services to leverage this report for rendering when a request is made by the user.  It also allows the report to take advantage of the SSRS 2008 features such as on-demand processing.

    If you want to upgrade the report prior to uploading to SSRS you will need to open it in Visual Studio 2008 (BIDS) or Report Builder v2, although that capability was not available in the RC1 release.  If you would like to figure out which engine is being used to process your report you can leverage the ExecutionLog2 view in the reporting services database.  There is a column called AdditionalInfo that has a <ProcessingEngine> element.  If that element is set to 2 it means that the 2008 processing engine was used, if set to 1 your report is using the 2005 processing engine.

  • Finding a SharePoint List Template Id

    There are several ways to find out the ID for a List.  A typical scenario where a list template id is required is querying a list or set of lists using SPSiteDataQuery.  You can find the list template id by looking at the 12\TEMPLATE\FEATURES directory and finding the list in question.  Opening the element manifest for the feature you will notice an attribute named DocumentTemplate, which is the List Template ID or Type which will provide the list template type, also ID for the out of box templates.

    <?xml version="1.0" encoding="utf-8" ?> 
    <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
      <ListTemplate Name="doclib" Type="101" 
        BaseType="1" OnQuickLaunch="TRUE" SecurityBits="11" 
        Sequence="110" DisplayName="$Resources:core,doclibList;" 
        Description="$Resources:core,doclibList_Desc;" Image="/_layouts/images/itdl.gif" 
        DocumentTemplate="101" /> 
    </Elements>

    Another method to do this leveraging the UI is to click Site Actions > Create.  The next screen provides a view of all the lists and document libraries that can be created on the site.  If you right click the list and select properties you will be presented the Url Address for the list.  Highlighting and scrolling to the end of the Url you will see a query string variable called ListTemplate.  The value presented is the list template ID.

    image

    A more dynamic method of finding this information in code is to leverage the object model.  SPWeb has a property called ListTemplates that returns a collection of the lists that can be created on the site.  The SPListTemplate class held in the collection has a property called DocumentTemplate property providing the List Template ID.

    using( SPSite siteCollection = new SPSite("http://moss.litwareinc.com") ) {
      using( SPWeb web = siteCollection.OpenWeb() ) {
        foreach( SPListTemplate template in web.ListTemplates ) {
          Console.WriteLine(template.DocumentTemplate.ToString());
        }
      }
    }

    Another little known method for getting the ID, at least for those features that are out of the box, is to use the feature GUID, which has a format of OOBFxxx-xxxx-xxxx-xxxx-xxxxxxxxx101.  For example, the listing for a document library is 00BFEA71-E717-4E80-AA17-D0C71B360101.

  • Unravel the SharePoint Threaded Discussion

    The SharePoint Threaded Discussion is a forum type of post.  Underneath, like all things SharePoint, it is a list.  By default that list is made up of two content types, Discussion and Message.  The content types have IDs of 0x012002 and 0x0107 respectively.  Following the content type inheritance chain this means that the Discussion content type inherits from folder and the Message content type from Item.  This gives us some clue about the structure of the list.

    If you enter a top level item in a threaded discussion list it will be created as a folder.  In order to navigate each top level thread using the object model we use code similar to that shown below.

    using ( SPSite siteCollection = new SPSite( "http://moss.litwareinc.com/" ) ) {
        using ( SPWeb web = siteCollection.OpenWeb( "/SiteDirectory/mktg" ) ) {
            SPList list = web.Lists["Team Discussion"];
    
            foreach ( SPListItem folder in list.Folders ) {
                Console.WriteLine( "-- Folder --" );
                Console.WriteLine( folder.Title );
                Console.WriteLine( folder.UniqueId.ToString() );
                Console.WriteLine( folder.ID.ToString() );
                Console.WriteLine( folder.Xml );
                Console.WriteLine( "-- End Folder --" );
            }
        }
    }

    You can also iterate over the list items themselves using the typical foreach convention.  When you do this take a look at the Xml property for the List Item and you will see various items that we can use to find the context of our item within the threaded discussion.

    <?xml version="1.0" encoding="utf-8" ?>
    <z:row xmlns:z='#RowsetSchema' 
           ows_ContentTypeId='0x010700135BC7419F95B04C8EA15325163C1444' 
           ows_Body='&lt;div class=ExternalClassAAF51B42E3834F8D90CA3A8DA7739064&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;br&gt;&lt;hr&gt;&lt;b&gt;From: &lt;/b&gt;System Account&lt;br&gt;&lt;b&gt;Posted: &lt;/b&gt;Wednesday, June 04, 2008 5:14 PM&lt;br&gt;&lt;b&gt;Subject: &lt;/b&gt;sample item one&lt;br&gt;&lt;br&gt;&lt;div class=ExternalClassF19C451E2C284124AB09D103E8A70A18&gt;&lt;div&gt;first sample item&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_TrimmedBody='&lt;div class=ExternalClass4D66DF23C3C649C4985CFE81A1E2E0E2&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_ParentFolderId='1' 
           ows_ID='2' 
           ows_ContentType='Message' 
           ows_Modified='2008-06-04 17:14:33' 
           ows_Created='2008-06-04 17:14:33' 
           ows_Author='1073741823;#System Account' 
           ows_Editor='1073741823;#System Account' 
           ows_owshiddenversion='1' 
           ows_WorkflowVersion='1'
           ows__UIVersion='512' 
           ows__UIVersionString='1.0' 
           ows_Attachments='0' 
           ows__ModerationStatus='0' 
           ows_SelectTitle='2' 
           ows_Order='200.000000000000' 
           ows_GUID='{8FAC9A20-6343-48A3-8BE3-CD7E17E45894}' 
           ows_FileRef='2;#SiteDirectory/mktg/Lists/TeamDiscussion/sample item one/2_.000' 
           ows_FileDirRef='2;#SiteDirectory/mktg/Lists/Team Discussion/sample item one' 
           ows_Last_x0020_Modified='2;#2008-06-04 17:14:33'
           ows_Created_x0020_Date='2;#2008-06-04 17:14:33' 
           ows_FSObjType='2;#0' 
           ows_PermMask='0x7fffffffffffffff' 
           ows_FileLeafRef='2;#2_.000' 
           ows_UniqueId='2;#{87C5CD04-7D68-4B26-B00C-DD76AE7B96D8}' 
           ows_ProgId='2;#' 
           ows_ScopeId='2;#{6ED2F3B8-3D0A-4B4F-B092-C9A6D1942AE2}' 
           ows__EditMenuTableStart='2_.000' 
           ows__EditMenuTableEnd='2'
           ows_LinkFilenameNoMenu='2_.000' 
           ows_LinkFilename='2_.000' 
           ows_ServerUrl='/SiteDirectory/mktg/Lists/Team Discussion/sample item one/2_.000' 
           ows_EncodedAbsUrl='http://moss.litwareinc.com/SiteDirectory/mktg/Lists/Team%20Discussion/sample%20item%20one/2_.000' 
           ows_BaseName='2_' 
           ows_MetaInfo='2;#' 
           ows__Level='1' 
           ows__IsCurrentVersion='1' 
           ows_ThreadIndex='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927' 
           ows_ShortestThreadIndexIdLookup='1;#' 
           ows_DiscussionTitleLookup='1;#sample item one' 
           ows_DiscussionTitle='sample item one' 
           ows_ReplyNoGif='SiteDirectory/mktg/Lists/Team Discussion/sample item one' 
           ows_ThreadingControls='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927' 
           ows_IndentLevel='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927' 
           ows_Indentation='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927' 
           ows_StatusBar='2008-06-05T00:14:33Z' 
           ows_BodyAndMore='&lt;div class=ExternalClassAAF51B42E3834F8D90CA3A8DA7739064&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;br&gt;&lt;hr&gt;&lt;b&gt;From: &lt;/b&gt;System Account&lt;br&gt;&lt;b&gt;Posted: &lt;/b&gt;Wednesday, June 04, 2008 5:14 PM&lt;br&gt;&lt;b&gt;Subject: &lt;/b&gt;sample item one&lt;br&gt;&lt;br&gt;&lt;div class=ExternalClassF19C451E2C284124AB09D103E8A70A18&gt;&lt;div&gt;first sample item&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_MessageBody='&lt;div class=ExternalClassAAF51B42E3834F8D90CA3A8DA7739064&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;br&gt;&lt;hr&gt;&lt;b&gt;From: &lt;/b&gt;System Account&lt;br&gt;&lt;b&gt;Posted: &lt;/b&gt;Wednesday, June 04, 2008 5:14 PM&lt;br&gt;&lt;b&gt;Subject: &lt;/b&gt;sample item one&lt;br&gt;&lt;br&gt;&lt;div class=ExternalClassF19C451E2C284124AB09D103E8A70A18&gt;&lt;div&gt;first sample item&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_BodyWasExpanded='{8FAC9A20-6343-48A3-8BE3-CD7E17E45894}' 
           ows_QuotedTextWasExpanded='{8FAC9A20-6343-48A3-8BE3-CD7E17E45894}' 
           ows_CorrectBodyToShow='&lt;div class=ExternalClass4D66DF23C3C649C4985CFE81A1E2E0E2&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;/div&gt;&lt;/div&gt;' ows_FullBody='&lt;div class=ExternalClassAAF51B42E3834F8D90CA3A8DA7739064&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;br&gt;&lt;hr&gt;&lt;b&gt;From: &lt;/b&gt;System Account&lt;br&gt;&lt;b&gt;Posted: &lt;/b&gt;Wednesday, June 04, 2008 5:14 PM&lt;br&gt;&lt;b&gt;Subject: &lt;/b&gt;sample item one&lt;br&gt;&lt;br&gt;&lt;div class=ExternalClassF19C451E2C284124AB09D103E8A70A18&gt;&lt;div&gt;first sample item&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_LimitedBody='&lt;div class=ExternalClassAAF51B42E3834F8D90CA3A8DA7739064&gt;&lt;div&gt;second sample item&lt;br&gt;&lt;br&gt;&lt;hr&gt;&lt;b&gt;From: &lt;/b&gt;System Account&lt;br&gt;&lt;b&gt;Posted: &lt;/b&gt;Wednesday, June 04, 2008 5:14 PM&lt;br&gt;&lt;b&gt;Subject: &lt;/b&gt;sample item one&lt;br&gt;&lt;br&gt;&lt;div class=ExternalClassF19C451E2C284124AB09D103E8A70A18&gt;&lt;div&gt;first sample item&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;&lt;/div&gt;' 
           ows_MoreLink='2' 
           ows_LessLink='2' 
           ows_ToggleQuotedText='2' 
           ows_Threading='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927' 
           ows_PersonImage='System Account' 
           ows_PersonViewMinimal='System Account' 
           ows_IsRootPost='0' 
           ows_ItemChildCount='2;#0' 
           ows_ServerRedirected='0'/>

    One of the more important properties is the ows_ParentFolderId.  By leveraging this property we can establish the parent thread or discussion, in this case the folder, that is the host for the item.  This will always be the top level folder item, not the item that was responded to originally.  While this does allow you to figure out which items go with which folder, it does not provide order or hierarchy.

    If you examine the default 'Threaded' view that comes with an instance of the Team Discussion you will notice that it contains one column, 'Threading (threaded)'.  This column is used for display and also used to sort the items.  There is a group of properties that provide a structure that enabling you to figure out he thread hierarchy.  Notice the ows_ThreadIndex, ows_Threading, ows_ThreadingControls, ows_IndentLevel, etc.  These properties all have the same value for an item, but as you navigate from item to item notice that a hierarchy of sorts is formed.  The base item has a thread value and then the children have this same thread value, but with some additional random text after the base item.  This begins to form a hierarchy. 

    ows_ThreadingIndex='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB’

    |_ows_ThreadingIndex='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B927'

                    |_ows_ThreadingIndex='0x01CA94B8313E972C16929A454ACFA141B2588F7291DB000004B9270000026E29'

    When reviewing the Xml attributes another item looks promising, ows_Order.  It has a format of xxx.xxxxxx.  This leads one to believe that this would control the order of the items in the thread.  I was hoping to find a pattern that was parent.childorder or something to that affect, but that is not the case.  Upon further examination this is simply the item id.  It is probably reserved for some future use.

  • SQL Server 2008 RC0 canonical errors during setup

    I was installing the RC0 version of SQL Server 2008 this weekend and kept running into the following error:

    This access control list is not in canonical form and therefore cannot be modified.

    There were a lot of these errors in the Feb CTP related to the registry which seemed to have been resolved.  The errors from the RC0 install were related to the directory structure and the permissions being out of order.

    I was able to resolve the issue by navigating to the %Program Files%\Microsoft SQL Server\100\Setup Bootstrap\Log\ directory and opening the details.txt file to find out the directory that was having the issue.  Using that information I went to the directory, right clicked and went to properties.  By clicking the 'Security' tab the OS warns you of the issue and then fixes it.  Hitting retry at this point allows the installation to continue.

    In total there were 5 directories that caused this issue, all of them located in the %Program Files%\Microsoft SQL Server directory structure.  While I didn't capture each of them it is an easy fix that produces a clean install of SQL Server.  Hopefully this will save you some time if you are running into the same problem.  I was installing on Windows Server 2003 EE R2 SP2.

  • Creating a 'Link to a Document' Item in a SharePoint Document Library programmatically

    One of the new features available in SharePoint 2007 are content types.  Content types allow you to classify content to be a particular type.  By marking content as a particular content type the metadata, workflow and policies associated with that content type are leveraged instead of default policies that may apply to a standard document.  One of the content types available is called 'Link to a Document'.  This content type allows you to store a link to a document in a document library instead of the document itself.  This comes in handy for documents that are stored in an http(s) referenceable location.  When an item is added to a document library and it is classified as a 'Link to a Document' content type an .aspx page is created and stored in the document library as a file.  This page is used to redirect the opening of the file to the location where the document link actually resides.  The content of the .aspx file is shown below.

    <%@ Assembly Name='Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' %>
    <%@ Register TagPrefix='SharePoint' Namespace='Microsoft.SharePoint.WebControls' Assembly='Microsoft.SharePoint' %>
    <%@ Import Namespace='System.IO' %>
    <%@ Import Namespace='Microsoft.SharePoint' %>
    <%@ Import Namespace='Microsoft.SharePoint.Utilities' %>
    <%@ Import Namespace='Microsoft.SharePoint.WebControls' %>
    
    <html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
    <Head> <META Name='progid' Content='SharePoint.Link'>
    <!--[if gte mso 9]><xml>
    <mso:CustomDocumentProperties>
    <mso:URL msdt:dt="string">http://moss.litwareinc.com/docs/my.xls, http://moss.litwareinc.com/docs/my.xls</mso:URL>
    <mso:ContentType msdt:dt="string">Link to a Document</mso:ContentType>
    </mso:CustomDocumentProperties>
    </xml><![endif]-->
    </head>
        <body>
            <form id='Form1' runat='server'>
                <SharePoint:UrlRedirector id='Redirector1' runat='server' />
            </form>
        </body>
    </html>

    If you want to create a 'Link to a Document' item programmatically, you can duplicate this file and replace the following string with the Url that matches the document where you want the item linked.

    <mso:URL msdt:dt="string">http://moss.litwareinc.com/docs/my.xls, http://moss.litwareinc.com/docs/my.xls</mso:URL>

    Create a template .txt file for this with the code below so that you can easily replace the url with your content link.  Notice the reference to {0}, {0}.  This will be used as a string replacement placeholder in the code snippet that will create the item in the document library.  In this example I have saved the file locally to the c: drive as linktodocumenttemplate.txt.

    <%@ Assembly Name='Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c' %>
    <%@ Register TagPrefix='SharePoint' Namespace='Microsoft.SharePoint.WebControls' Assembly='Microsoft.SharePoint' %>
    <%@ Import Namespace='System.IO' %>
    <%@ Import Namespace='Microsoft.SharePoint' %>
    <%@ Import Namespace='Microsoft.SharePoint.Utilities' %>
    <%@ Import Namespace='Microsoft.SharePoint.WebControls' %>
    
    <html xmlns:mso="urn:schemas-microsoft-com:office:office" xmlns:msdt="uuid:C2F41010-65B3-11d1-A29F-00AA00C14882">
    <Head> <META Name='progid' Content='SharePoint.Link'>
    <!--[if gte mso 9]><xml>
    <mso:CustomDocumentProperties>
    <mso:URL msdt:dt="string">{0}, {0}</mso:URL>
    <mso:ContentType msdt:dt="string">Link to a Document</mso:ContentType>
    </mso:CustomDocumentProperties>
    </xml><![endif]-->
    </head>
        <body>
            <form id='Form1' runat='server'>
                <SharePoint:UrlRedirector id='Redirector1' runat='server' />
            </form>
        </body>
    </html>

    Once you have that file in place the following code snippet can be used to create the item in the document library.

    using ( SPSite siteCollection = new SPSite( "http://moss.litwareinc.com" ) ) {
        using ( SPWeb web = siteCollection.OpenWeb( "docs" ) ) {
            SPList list = web.Lists["Sample"];
    
            //link to the file
            string fileLinkUrl = "http://moss.litwareinc.com/docs/Shared%20Documents/ConfigureIRMinWSS30.doc";
    
            StringBuilder builder = new StringBuilder();
    
            using ( TextReader reader = new StreamReader( @"C:\linktodocumenttemplate.txt" ) ) {
                builder.Append( reader.ReadToEnd() );
            }
    
            //replace string template with values
            builder.Replace( "{0}", fileLinkUrl );
            
            //should change the name of the .aspx file per item
            SPFile file = list.RootFolder.Files.Add( "link_title.aspx", UTF8Encoding.UTF8.GetBytes(builder.ToString()));
    
            //set list item properties
            SPListItem item = file.Item;
            item["Content Type"] = "Link to a Document";
            SPFieldUrlValue itemUrl = new SPFieldUrlValue();
            itemUrl.Description = "From sample code";
            itemUrl.Url = fileLinkUrl;
            item["URL"] = itemUrl;
            //persist changes
            item.Update();
        }
    }
  • SharePoint Webs web service CreateContentType and UpdateContentType

    I was experimenting with the SharePoint Webs web service CreateContentType and UpdateContentType methods recently.  The goal was to create a base content type and update it by adding an additional column with a separate web service call.  According to the documentation you are suppose to use the <FieldRefs><FieldRef /></FieldRef> Xml structure to create an XmlNode node and pass into to the method.  Well, the documentation is wrong.  Thank goodness I ran across this post in the forum which pointed me in the right direction.

    The proper Xml structure for the fields is shown below:

    <Fields>
        <Method ID="1">
            <Field ID="{246D0907-637C-46b7-9AA0-0BB914DAA832}" Name="_Author" DisplayName="Author" Hidden="FALSE" />
        </Method>
        <Method ID="1">
            <Field ID="{038D1503-4629-40f6-ADAF-B47D1AB2D4FE}" Name="Company" DisplayName="Company" Hidden="FALSE" />
        </Method>
        <Method ID="1">
            <Field ID="{0FC9CACE-C5C2-465d-AE88-B67F2964CA93}" Name="_Category" DisplayName="Category" Hidden="FALSE" />
        </Method>
        <Method ID="1">
            <Field ID="{1DAB9B48-2D1A-47b3-878C-8E84F0D211BA}" Name="_Status" DisplayName="Status" Hidden="FALSE" />
        </Method>
        <Method ID="1">
            <Field ID="{52578FC3-1F01-4f4d-B016-94CCBCF428CF}" Name="_Comments" DisplayName="Comments" Hidden="FALSE" />
        </Method>
    </Fields>

    The method ID attribute is an indicator, apparently, on what to do with the field.  ID=1 means add it to the content type, ID=2 means update the field and ID=3 indicates you want the field deleted.  This seems a bit odd since you are passing a separate XmlNode for newFields in the CreateContentType() method and in the UpdateContentType method passing newFields, updateFields and deleteFields as separate parameters.  Shouldn't that be enough to let the method know what to do with each node?

    Hopefully this will save others some time in the future.  In the meantime, the documentation needs to be updated properly to reflect the expected structure.

  • Adding Web Parts to the Web Part Gallery Using the SharePoint Object Model

    Since my first post was based on removing web parts from the web part gallery of a 'My Site' there have been questions about how to add the web parts back should circumstances require it.  This is pretty straight-forward, but can get more interesting depending on how robust you want to make the end solution.  The Web Part Gallery is a document library that holds a reference to a web part file, either a .dwp or .webpart file.  This file is simply a metadata definition of the web part.  To add a web part back in the gallery we can leverage the code shown below.  This assumes that the web part assembly is installed and the safe control entries are made in the web.config file.

    public static void IterateWebPartGallery(string siteCollectionUrl) {
        string filePath = @"C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\PortalLayouts\DWP\owa.dwp";
    
        using ( SPSite site = new SPSite( siteCollectionUrl ) ) {
            using ( SPWeb web = site.OpenWeb() ) {
    
                SPList webPartGallery = web.GetCatalog( SPListTemplateType.WebPartCatalog );
    
                SPFolder folder = webPartGallery.RootFolder;
                if( folder != null ) {
                    byte [] webPartFile = File.ReadAllBytes(filePath);
                    FileInfo webPartFileInfo = new FileInfo(filePath);
    
                    folder.Files.Add(webPartFileInfo.Name, webPartFile);
                    folder.Update();
                }
            }
        }
    }

    The code gets a reference to the web part gallery in the target site collection by calling the SPWeb.GetCatalog() method.  Once we have that reference we get a reference to the root folder of the gallery and call the folder.Files.Add() method to add the web part file.  Calling folder.Update() persists this information to the database.

    While this will simply add the file by parsing its contents it won't give us the fine grained control we may want for adding our web part to the gallery.  If the file uses resources to define the title and description properties then those get loaded into the gallery "as is" using the method above.  This makes our title $Resources:spscore,PeopleWebParts_OWA_Title_Text; and the description $Resources:spscore,PeopleWebParts_OWA_Desc_Text;.

    This can be made a bit more robust by figuring out the title and description from the resource file and setting them in the gallery when the web part is loaded.  The SPFolder.Files.Add() method has an overload that accepts a Hashtable reference where we can set additional properties for the web part.  The resource values above indicate the resource file is the spscore resource file, spscore.en-US.resx for the en-US culture.  Resource files in SharePoint are stored in the \12\Resources directory.  I tried to load this file directly using the ResXResourceReader, but received an exception about a missing 'resheader' value in the file.  Since .resx files are just Xml I decided to use an XPathNavigator and parse the values out that way.  You could also dynamically parse the .webpart or .dwp file to figure out what the associated resx file the web part is referencing, but that is beyond the scope of this post.  The final code snippet is shown below.

    string filePath = @"C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\FEATURES\PortalLayouts\DWP\owa.dwp";
    string resourceDirectory = @"C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\Resources";
    
    using ( SPSite site = new SPSite( siteCollectionUrl ) ) {
        using ( SPWeb web = site.OpenWeb() ) {
    
            SPList webPartGallery = web.GetCatalog( SPListTemplateType.WebPartCatalog );
    
            string title = "";
            string description = "";
    
            XPathDocument xml = new XPathDocument( File.Open( Path.Combine( resourceDirectory, "spscore.en-US.resx" ), FileMode.Open, FileAccess.Read ) );
            XPathNavigator navigator = xml.CreateNavigator();
            string xpathExpression = "/root/Data[@Name='{0}']/Value";
            XPathNavigator node = navigator.SelectSingleNode( string.Format( xpathExpression, "PeopleWebParts_OWA_Title_Text" ) );
            if ( node != null ) {
                title = node.Value;
            }
            node = navigator.SelectSingleNode( string.Format( xpathExpression, "PeopleWebParts_OWA_Desc_Text" ) );
            if ( node != null ) {
                description = node.Value;
            }
    
    
            SPFolder folder = webPartGallery.RootFolder;
            if ( folder != null ) {
                byte[] webPartFile = File.ReadAllBytes( filePath );
                FileInfo webPartFileInfo = new FileInfo( filePath );
    
                Hashtable properties = new Hashtable();
                properties.Add( "Title", title );
                properties.Add( "WebPartDescription", description );
                properties.Add( "Group", "Outlook Web Access" );
    
    
                folder.Files.Add( webPartFileInfo.Name, webPartFile, properties );
                folder.Update();
            }
        }
    }
More Posts Next page »

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker