Wednesday, June 27, 2012

Custom Pager for SPGridView - Part 1

The problem:

My customer requirement is that the web part has to have a gridview whose pager allows access to any given page, go to the next page, go to the previous page, and select how many rows are displayed per page.

The solution:

This seemingly simple problem is actually a little more complex than it seems. The issue is that SPGridView does not have an event that triggers when the page size changes. So, the first step will be to create a new class that inherits from SPGridView and contains the following three things:

1)  The delegate PageSizeChangeHandler;

2) The public event PageSizeChanged;



3) the overridden RaisePostBackEvent method which triggers the PageSizeChanged event if the event argument is "Rows$" followed by the number of rows (e.g., "Rows$5" for 5 rows.) The dollar sign is used as a separator to follow the guidelines for event arguments for gridviews:



Of course, there should also be a new class PageSizeChangeEventArgs (inheriting from System.EventArgs) with a public property NewPageSize and a public constructor that sets this property based on the argument constructor:


Now we have a gridview capable of raising a postback event when the number of rows to be displayed changes.

To be continued...




Wednesday, June 20, 2012

Capturing Web Analytics Data in a Custom Web Part



The problem:

I need to display the number of daily unique visitors to a site collection on a custom web part. Using the following code results in an array with the only element being "The specified user or domain was not found." The Web application uses claims authentication.


The solution:

What is causing this issue?

The problem is that the GetWebAnalyticsReportData() function takes the user login name from Thread.CurrentPrincipal.Identity.Name and passes it to SPWeb.DoesUserHavePermissions() - but SPSecurity.RunWithElevatedPrivileges does not change Thread.CurrentPrincipal.Identity.Name to the application pool account name. It does, however, change WindowsIdentity.GetCurrent() to the application pool account.

So, it is necessary to explicitly set Thread.CurrentPrincipal to the correct identity. Since my Web application uses claims authentication, here is a correct way to do this:

System.Threading.Thread.CurrentPrincipal = 
 ClaimsPrincipal.CreateFromIdentity(WindowsIdentity.GetCurrent());

Works like a charm!

Tuesday, June 19, 2012

Expanding/Collapsing Web Parts

The problem:

My requirements state that a certain web part on my page should be expandable/collapsible. When the user presses the "-" sign this web part is to collapse, whereas when she presses the "+" sign this web part is to expand. When the page first loads, this web part should be collapsed.

The solution:

We should use jQuery for this purpose. First, we need to find out the ID of the web part in question (it should start with "MSOZoneCell_", something like "MSO_ZoneCell_WebPartctl00_m_MyCoolWebPart") - view your web part page in the source and find the ID there.

Then, add a new Content Editor Web Part to your page. In the CEWP, in the HTML source, type this:

<div align="right" class="content" id="jLoadDiv"><a href="javascript:show(&#39;MSOZoneCell_WebPartctl00_m_MyCoolWebPart&#39;);"><img title="Show" id="plus" alt="Show" src="/_layouts/images/EXPAND.GIF"/></a> <a href="javascript:hide(&#39;MSOZoneCell_WebPartctl00_m_MyCoolWebPart&#39;);"><img title="Hide" id="minus" alt="Hide" src="/_layouts/images/COLLAPSE.GIF" style="display: none"/></a> </div>
<script type="text/javascript">

$("document").ready(function()
{
$('td[id*="MSOZoneCell_WebPartctl00_m_MyCoolWebPart"]').css("display", "none");
$('img[id*="minus"]').css("display", "none");
});
function show(webpartID)
{
$('td[id*="'+webpartID+'"]').css("display", "block");
$('img[id*="plus"]').css("display", "none");
$('img[id*="minus"]').css("display", "block");
}
function hide(webpartID)
{
$('td[id*="'+webpartID+'"]').css("display", "none");
$('img[id*="minus"]').css("display", "none");
$('img[id*="plus"]').css("display", "block");
}</script>

Make sure you set the Chrome to None on your CEWP.

That's it. This does the job. You can even export the CEWP and then add its contents (with the contents of the Content element being HTML-encoded - use  this tool to perform the encoding) to the page or elements file.

Monday, June 11, 2012

DateTime Format With Time Zones

The problem:

In my Visual Web Part, I have to display a current date in the following format:

1:29 pm EDT June 11, 2012

The usual way to display the current date in this format is via <%=DateTime.Now.ToString("H:MM tt K MMMM d, yyyy"). The problem with this approach, though, is that the time zone is displayed in the wrong format, more like

1:29 pm -04:00 June 11, 2012

The solution:

First, we need to find the acronym current time zone. The following two functions take  care of it:


Next, we need to split the formatted date string (where the white space is the delimiter), and replace the second element (i.e., "-04:00") with the proper acronym (i.e., "EDT.") The following code takes care of it:


Finally, in the code-behind for the user control for the Web Part, create a protected method (e.g., ToProperFormat()) and make it return DateTime.Now.ToProperFormat(). Then, in the ASCX file, call <%=ToProperFormat() %> . Note that calling the extension method directly from the ASCX file results in a compilation error unless you import the extension namespace.

Tuesday, May 22, 2012

Site Pages in Non-Team Sites

The problem:

I am creating a site from my site collection feature receiver; the site is not a team site, but it does need to have the Site Pages library. You might recall that on every team site this library is automatically provisioned, because of  the WikiPageHomePage feature; however, the problem is that this feature cannot be activated in non-team sites.

The solution:

Since I have to activate the feature in code, the best way to do it is to obtain the reference to the site that is being created and then call the EnsureSitePagesLibrary():

SPWeb web = ...
SPList list = web.Lists.EnsureSitePagesLibrary();

Note that, in a similar fashion, you can provision a Site Assets library in a non-team site - just call EnsureSiteAssetsLibrary(), rather than EnsureSitePagesLibrary().

Wednesday, May 9, 2012

Caching Web Part Content

The problem:

Here is a common scenario. You have a web part whose contents include a gridview, which is populated by going through all (or many) subsites and gathering the information from each of these sites. The gridview has to allow sorting and possibly even filtering.

The problem, though, is that sorting and filtering typically cause postback, which means you need to perform the expensive operation of loading the data again. If the data happens to be static (or relatively static), you wind up loading the same data over and over again, which results in performance deterioration.

The solution:

The solution depends on whether the data displayed the gridview is the same for every user.

a) If it is the same, then it will be worthwhile to use the Cache object and add the datatable (or any other data source object) to the cache if it is not already there. This will speed up the loading time for every user, since the cache will be populated only once. A couple of gotchas, though:

1) Make sure you place a lock around the code that accesses cache. Otherwise, if it takes 15 seconds to load the data table and many users are trying to access this page at the same time, then all of these users will be simultaneously trying to update the same cache object - and this would result in performance problems.

2) If the data occasionally changes, it would be wise to implement cache dependency, so that the cache is invalidated whenever the data changes.

b) If it is not the same, then use Session instead of Cache. But, make sure the session state is enabled. Another gotcha is that there is no expiration of the Session object, nor there is anything equivalent to cache dependency for Session objects - so you'll need to find some other way to clear the session object once the underlying data table changes.

Tuesday, May 8, 2012

Provisioning Site Pages

Today I'd like to talk about provisioning pages in a feature. Sometimes your customer wants to add some content to the pages you provision, and/or to have some web parts on them.. If that's the case, application pages will not do - they live on the file system (more specifically, LAYOUTS on the hive, or some subfolder of LAYOUTS), and you cannot add web parts to them.

Provisioning site pages using Visual Studio 2010 is a little harder than provisioning application pages - after all, there is no such an item template as "Site Page" or "Web Part Page." So, you will need to use a workaround.

First of all, you need to remember that when you provision files, you do so via modules. Luckily, there is such an item template. When you create a module, an Elements.xml file and a Sample.txt files are added to your Visual Studio 2010 project.

Rename Sample.txt to YourSitePage.aspx (or whatever name you'd like to use for your page), and add the following:


Note that in the PlaceHolderMain, I am creating a new web part zone, called MainWebPartZone (feel free to rename it if you don't like this name.) This is where the web part will live on this page.

Also add a class to your module and name it YourSitePage.aspx.cs.  This will be the code-behind class for your page.

The next step is configuring the module. If you want your user to treat your page as a library item, you need to provision it as ghostable in library. In the CDATA section, insert the full contents of the .webpart file (except the processing instruction!) In my example, the new page is provisioned in the Pages library.