Thursday, January 12, 2012

How to download a document with Save As Dialog in SharePoint 2010 programmatically

There are many posts which describe how to download a document with Save As Dialog in ASP.NET. I found a good article which was written by Rick Strahl. This can be easily achieved with just 4 lines of code as follows -

Response.ContentType = "application/ms-word";

Response.AddHeader("Content-Disposition", "attachment; filename=" + file.Name);

Response.TransmitFile( Server.MapPath(relativeUrl) );

Response.End();

However, its a different ball game when it comes to SharePoint. As we all know documents uploaded to any document library are stored in the database and when we use the above code snippet we get the error that the url is not a valid path.

The crux lies in getting the file object from the SPListItem that we get from the ListItemCollection, and write the file as a binary stream in the Response. The following code should help you achieve this -

private void OpenDocument(SPListItem listItem)
        {
        SPFile file = listItem.File;
            using (MemoryStream ms = new MemoryStream(file.OpenBinary()))
            {
                Response.Clear();
                // Clear the content of the response
                Response.ClearContent();
                Response.ClearHeaders();

                // Buffer response so that page is sent after processing is complete.
                Response.BufferOutput = true;

                // Add the file name and attachment, which will force the open/cance/save dialog to show, to the header
                Response.AddHeader("Content-Disposition", "attachment; filename=" + file.Name);
               
                // Add the file size into the response header
                Response.AddHeader("Content-Length", file.Length.ToString());

                // Set the ContentType - octet stream helps handle all content types generically
                Response.ContentType = "application/octet-stream";

                ms.WriteTo(Response.OutputStream);

                Response.Flush();
                Response.Close();
            }
}

The above code handles all content types and we need not explicitly get the content type of the document.

Friday, November 18, 2011

Replicating the Text Editor Functionality Programmatically

I was developing a custom webpart which has custom properties viz. a people picker, a textbox to type a question and another to type the answer.


As, I was using the EditorPart and the PeoplePicker control, I was unable to group the People Picker property with the other two properties. I would get the People Picker at the top of the webpart property pane, and the other two at the bottom of the properties pane under the custom group "My Properties". Any blogger who knows how to group your custom properties can please post the related url. Thanks in advance.

Thus, I created two more additional textboxes to capture the question and the answer, and was in high spirits of having completed my task.When I demonstrated this "cool" webpart to my product owner, he casually added that the answer textbox needs to have a pop-up Text Editor, as you see in any OOTB textboxes which help you build content. Thats when the real problem started. 

I was expecting Microsoft to give me a "SPLinkBuilderXXX" kind of a control which gives me this functionality straightaway, just as the PeoplePicker class. I was very happy to find on MSDN that there did exist a class called "TextZoomBuilder" class, which to my dismay was marked "Obsolete" :(.

I found that MS uses an input button which calls a javascript function in javascript:MSOPGrid_doBuilder, and passes the arguments - '_layouts/1033/zoombldr.aspx', the id of the control to which the value needs to passed, and the properties of the pop-up window.

When I tried to replicate this, I found that SharePoint (the ASP.NET runtime engine) appends dynamic Guids to all the controls on a page to prevent them from getting duplicated. Thus, we cannot get the ClientId of the control at runtime in the code-behind. It appears this is achieved by SharePoint using the 'ContentToolPart' which is an internal sealed class.
I am very thankful to Tony Bierman who posted Leveraging Custom Property Builders in SharePoint Web Parts, a wonderful article in 2006. The workaround being to place a hidden form field on the page to which you can get the zoombldr.aspx page to post the data back to, and then use a Javascript function which simulates the 'Apply' button click action of the webpart properties pane to post the value back to your textbox.

However, when I tried to implement it in SharePoint 2010, I got lot of Javascript errors. I found that we need to use the name of ApplyButton directly. The code which worked for me was - 

 string key = "applyFunction";
                StringBuilder embeddedScriptFormat = new StringBuilder("<script language=jscript>function ApplyProperties(){\n");
                embeddedScriptFormat.Append("document.forms[MSOWebPartPageFormName].MSOTlPn_Button.value = 'apply';\n");
                embeddedScriptFormat.Append("document.forms[MSOWebPartPageFormName].elements['ctl00$MSOTlPn_EditorZone$MSOTlPn_AppBtn'].click();\n");
                embeddedScriptFormat.Append("}\n");
                embeddedScriptFormat.Append("</script>\n");
                if (!Page.ClientScript.IsClientScriptBlockRegistered(key))
                {
                    Page.ClientScript.RegisterClientScriptBlock(this.GetType(), key, embeddedScriptFormat.ToString());
                }


One more trick up your sleeve as a SharePoint developer.

Friday, November 11, 2011

Custom Pagelayout with custom user controls causes a page crash

So this was an issue that we were facing a couple of days back and really had a tough time solving it, thanks to the wierd behaviour of SharePoint.

The Issue:

Ok, as any other project in SharePoint would do we were using custom pagelayouts for our content pages. However, one of the layouts has a user control which would push a couple of webparts into the different webpart zones of layout. This page layout would crash when it gets applied to any page in the site.
Applying Custom Page layout

Investigations:

As is common in most of the issues in SharePoint, the ULS wouldnt give a clue. We used to get wierd error messages viz. Unknown class for webpartzone 'xyz', div tag is not a valid tag etc.

The interesting part was that trying to debug wouldnt even hit the breakpoint in any of the pages.

Thus, we turned to the only resort for any developer, Google :)

Cause:

We found that SharePoint would introduce two error webparts into the pagelayout while deploying the solution. This could be checked by going to the edit properties of the pagelayout in the master pages and pagelayouts gallery of the site collection. Scrolling down to the end of the page and selecting the option to 'Open the page in Webpart maintenance mode'.
Link to Webpart Page Maintenance Link at the bottom

Now the question is from where have these errorwebparts come from? Obviously, no sane developer would do that.

Root Cause:

It was obvious that SharePoint was pushing these, but why? A lot of googling ultimately pointed the issue. As I understand SharePoint expects the '<ZoneTemplate></ZoneTemplate>' tags to be defined within the Webpartzone tags, failing to which it injects these webparts when your custom code tried to insert webparts into the webpartzone.

Resolution:

Well the first step in trying to resolve this was to remove the error webparts from the custom pagelayout, and checking if the issue is gone. Having confirmed that, we added the '<ZoneTemplate></ZoneTemplate>' tags at all places in the page before the closing tage of the webpartzone.

Well abracadabra............. the problem is gone :)

Hope this helps you in solving your problem too.

Tuesday, September 13, 2011

SP 2010 Best Practices


1. Ensure Object Disposal by using Dispose method, using clause or try, catch, and finally blocks

2. Create lists and libraries using appropriate list definition templates
3. Refactor your project solution into appropriate projects to have clear demarcation of various object. Use a separte project for data access, a separate project for list instantiation, and a separate project for logging, and another project for SharePoint artefacts like webparts, custom pages etc
4. Always use the developer dashboard on all the custom pages that are developed. This gives an overview of the performance of the page
5. The SPSiteCollection.Add method creates and returns a new SPSite object. You should dispose of any SPSite  object returned from the SPSiteCollection.Add method.
6. The SPSiteCollection [] index operator returns a new SPSite  object for each access. An SPSite instance is created even if that object was already accessed.
7.The SPSite.AllWebs.Add method creates and returns an SPWeb  object. You should dispose of any SPWeb object returned from SPSite.AllWebs.Add.
8.If the object is obtained from the SharePoint context objects (GetContextSite  method and GetContextWeb method), the calling application should not call the Dispose  method on the object. Doing so may cause the SharePoint object model to behave unpredictably or fail. This is due to an internal list that is kept in the SPSite and SPWeb objects derived in this way. Internally, the object model enumerates over this list after page completion to dispose of the objects properly.
9. Always use SPQuery object and CAML Query to filter data from lists. Iterate through collections only after filtering the collection as required
10. While using images always set the ALT property to the IMG tag.
11. Create lists and columns with a name WITH NO SPACES
12. WebParts: Always initialize your controls in the CreateChildControls method.
Call the EnsureChildControls method in your code
Do not use the Render() method while developing a webpart in SharePoint
13. Frequently check the entries in the log for - The total number of SPRequest objects, An SPRequest object continues, An SPRequest object was garbage collected
14.Make sure that the containers like lists and libraries follow the recommended guidelines on number of items in it viz 2000 items in SharePoint 2007 and 5000 items in SharePoint 2010
   a. Implement a timer job that deletes items from list regularly if they are not needed
   b. Store items in folders rather than storing them directly in list
15.Use the SPQuery.ViewFields property to get only the required number of fields in the dataset
16. SPDisposeCheck tool on all code before deploying outside of you development environment
17.Wrap all your custom code that runs with full trust in SharePoint using the SPMonitoredScope class.

using (new SPMonitoredScope("My Code"))
{
    //Your code
}


using (new SPMonitoredScope("My Scope Name",1000,new SPRequestUsageCounter(3),new SPSqlQueryCounter()))
{
    //Your code
}

You can use any of the following classes to monitor specific code sections depending on the scenario SPCriticalTraceCounter (critical events and asserts), SPExecutionTimeCounter (track execution time for your scope), SPRequestUsageCounter (rack the number of SPRequest objects your code uses), SPSqlQueryCounter (tracks the number of SQL queries for your scope).
18. Database related -
       a. SharePoint runs a nightly timer job to rebuild indexes and update statistics, so be sure that this job is created and running.
       b. Always pre - grow all your databases, as it is an expensive operation for SQL Server to autogrow databases
19. Do not use CSS inline styles but use CSS files and appropriately markup the CSS styles using theme attributes
20. At all possible times develop sand-boxed solutions, and use full-trust proxies to do the necessary code plumbing
21.Allocate a pre-defined quota to all your sandboxed solutions, and regularly monitor their usage and fine tune them
22. Validate all the sandboxed solutions using the SPSolutionValidator class
23. The 'site.GetFile' method returns an SPFile object even when you pass the path to a site page that does not exist.You should inspect the Exists property before attempting to access any of its other methods.
24. Recompile all your sandbox solutions by referencing it to the Sandboxed Microsoft.SharePoint.dll present in the UserCode directory in 14 hive to make sure that none of the API are calling methods which are restricted in SSs.
25. Indexing should be done on the columns that are used for filtering in the LINQ/CAML queries
26. use Power Shell scripts for deployment 
27. Do not use the Workflow History list heavily. Implement some other logging mechanism if needed to log heavily
28. Make sure to create a root site in a site collection for a given web application, else it might give issues when accessing web services via that web application
29. Use SPWeb.ProcessBatchData method for bulk deletion of items in a list
30. Do not use delay activities in custom workflows as they do not fire always. It is a known issue. Try to implement Timer Jobs instead
31. In scenarios where a field’s display name could change, it is a safer bet to access the field within a Fields collection using the GetFieldByInternalName method
32. Create Site columns and custom content types in the site columns gallery or content types gallery of top-level sites so that they are made available on a site collection–wide basis
33. Always use the SPList.EventReceivers.Add(GUID) overload while registering event handlers programmatically. This helps to check if the event handler is already existing or not, and its easier to delete it in Feature Deactivation
34. It is generally recommended that you avoid custom site definitions when designing and developing SharePoint solutions

Thursday, October 21, 2010

Getting Started

After trying all I could to use the VMs in the traditional way, I stumbled upon a post about booting into VHD.

To use the VMs created or shared by Microsoft, we need to use Win2k8 Server, with Hyper-V enabled. Unfortunately, the processor on my laptop (Lenovo G450) in not virtualization enabled. Thus, I was looking for other alternatives, which would not keep me tied to the monolithic server in the office which hosts all the VMs.

Laptops which have Windows 7 OS, can be made to boot into a VHD, using few simple steps -


Step 1: Open a elevated command prompt.
Step 2: Type this command: bcdedit /copy {current} /d "<any name for the VHD file>"
If the command succeeds, BCDEdit displays a message similar to the following: The entry was successfully copied to {CLSID_Number}
Step 3: Then type this command bcdedit /set {CLSID_Number} osdevice vhd=[C:]\disk1.vhd



To confirm the settings simply type bcdedit


If you want to delete the entry make note of the GUID listed in bcdedit and use the following command

bcdedit /delete {GUID} /cleanup


That's it. The next time you boot your laptop, you will find an option to either boot into your original OS (Win 7) or into the VHD.

The beauty of this approach is that you need not make your system dual bootable (in the real sense), and at the end of the day, all the changes you do to your VHD file are just changes made to a file. Thus, if you are archiving your VHD file at regular intervals (say after installing SQL Server, then after installing SP, and so on), you just need to replace your earlier file to roll back to an earlier step.

Thus, I now have seven versions of the VHD, and I can roll back to any step at any moment :)

Also, unlike the VMs using VPC etc, you are not restricted to only a certain percentage of the underlying hardware, as you will need some memory to run the virtualization software and the base OS, you can use the whole hardware resources at your disposal as it boots just like a dual bootable system.

So there you go, all set to delve into the various intricacies of SP 2010, having taken care of the VM/installation issues.

I shall be writing more on the various explorations that I shall do with SP 2010 in coming days, till then au'revoir....