The Black Knight Sings

Songs about SharePoint and other adventures by Per Jakobsen


Date: # Monday, July 20, 2009

Title: List Quota and Locklevel of all Site Collections in Farm


Here is another small utility requested in one of the SharePoint newsgroups

It creates a tab delimited list of all the Site Collections in the current farm

using System;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
 
namespace ListQuotas
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Site Coll name\tSite Coll Url\tIs ReadLocked\tIs WriteLocked\tQuota in bytes");
            SPSecurity.RunWithElevatedPrivileges(delegate
            {
                foreach (SPWebApplication webapp in SPWebService.ContentService.WebApplications)
                {
                    foreach (SPSite sitecoll in webapp.Sites)
                    {
                        Console.WriteLine("{0}\t{1}\t{2}\t{3}\t{4}",
                            sitecoll.RootWeb.Title,
                            sitecoll.Url,
                            sitecoll.ReadLocked,
                            sitecoll.WriteLocked,
                            sitecoll.Quota.StorageMaximumLevel);
                        sitecoll.RootWeb.Dispose();
                        sitecoll.Dispose();
                    }
                }
            });
        }
    }
}

The executable can be found here and the source code here



Monday, July 20, 2009 6:34:20 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Title: List all SharePoint groups a user belongs to


This is just a small Utility requested on one of the SharePoint Newsgroups

It uses the UserGroup web service to list all the SharePoint Groups a user belongs to in a specified Site Collection

It's a small console application which requires two parameters:
···The first parameter is the Url of the site collection including protocol (example: http://localhost)
···The second parameter is the full username including Domain (example: MOSSWORK\user)

using System;
using System.IO;
using System.Xml;
using ListUsersGroups.ug;
 
namespace ListUsersGroups
{
    class Program
    {
        static int Main(string[] args)
        {
            if (args.Length < 2)
                return help();
            try
            {
                UserGroup usergroup = new UserGroup();
                usergroup.UseDefaultCredentials = true;
                usergroup.Url = args[0]+"/_vti_bin/usergroup.asmx";
                XmlNode groupXml =usergroup.GetGroupCollectionFromUser(args[1]);
                XmlDocument doc = new XmlDocument();
                doc.Load(new XmlNodeReader(groupXml));
                XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable);
                nsmgr.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/soap/directory/");
                foreach (XmlNode group in doc.SelectNodes("//sp:Groups/sp:Group", nsmgr))
                    Console.WriteLine(group.Attributes["Name"].Value);
            }
            catch (Exception ex)
            {
                Console.WriteLine("{0}\r\n{1}", ex.Message, ex.StackTrace);
            }
            return 0;
        }

        private static int help()
        {
            Console.WriteLine(@"{0} ", Path.GetFileNameWithoutExtension(Environment.GetCommandLineArgs()[0]));
            Console.WriteLine(@"  Site-url: Url of site collection including protocol (example http://localhost)");
            Console.WriteLine(@"  username: Full username including domain (example MOSSWORK\user)");
            return -1;
        }
    }
}

This execuable can be found here and the source code here



Monday, July 20, 2009 2:01:05 PM (Romance Standard Time, UTC+01:00)  #    Comments [2]

Date: # Sunday, April 26, 2009

Title: Giving end users a list of all the SharePoint sites they have access to


When You’re running a medium sized SharePoint Farm you may often get requests from end users for a list of all the SharePoint sites they have access to.

I’ve implemented a small feature which provides this information as an application page, which you can access through a new menu item on the Welcome menu.

The implementation of this is quiet simple:

Add a menuitem to the Welcome menu pointing to our own application page:

<CustomAction Id="6346660F-707E-40d2-A7CF-AB6BAC7A98FD"
              Location="Microsoft.SharePoint.StandardMenu"
              GroupId="PersonalActions"
              Sequence="400"
              Title="Show All Sites"
              Description="Shows all sites which you have access to">
    <UrlAction Url="_layouts/ShowAllSites/ShowAllSites.aspx" />
</CustomAction>

 

The application page just contains a ASP.NET TreeView control and the following code:

Get Login of current user:

login = SPContext.Current.Web.CurrentUser.LoginName;

 

Loop through all SiteCollections inside RunWithElevatedPrivileges:

SPSecurity.RunWithElevatedPrivileges(delegate()
{
    SPWebService cs = SPWebService.ContentService;
    foreach (SPWebApplication wa in cs.WebApplications)
    {
        foreach (SPSite site in wa.Sites)
        {
            using (SPWeb web = site.RootWeb)
            {
                AddWebToTreeIfAccess(web, tvSites.Nodes);
            }
            site.Dispose();
        }
    }
});

 

Skip Shared Services Administration and My Sites:

if (web.WebTemplate == "OSRV"
 || web.WebTemplate == "SPSMSITEHOST"
 || web.WebTemplate == "SPSPERS")
    return;

 

Process all child sites:

TreeNode tn = new TreeNode();
foreach (SPWeb childweb in web.Webs)
{
    AddWebToTreeIfAccess(childweb, tn.ChildNodes);
    childweb.Dispose();
}

 

Check if user can view pages on this site:

bool userHasAccess = web.DoesUserHavePermissions(login, SPBasePermissions.ViewPages);

 

Add site to treeview if user can access it or any of it's decendents:

if (userHasAccess
 || tn.ChildNodes.Count > 0)
    {
    if (userHasAccess)
    {
        tn.Text = web.Title;
        tn.NavigateUrl = web.Url;
    }
    else
    {
        tn.Text = "(" + web.Title + ")";
        tn.SelectAction = TreeNodeSelectAction.Expand;
    }
    treeNodeCollection.Add(tn);
}

A prebuilt solution package can be found HERE and the full source code HERE



Sunday, April 26, 2009 3:59:37 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Date: # Friday, December 19, 2008

Title: Custom pagers for SPGridView


I don't think that the built in pagers for SPGridView are the most useful and informative, so for one of my current projects I decided to implement two custom pagers

The first was XofYPager which shows the page numbers like this:


The second was SmartPager which shows the page numbers like this:


The use of these pagers are very simple just include the classes below and then before you DataBind() your SPGridView set the PagerTemplate property to an instance of one of these classes.

The XofYPager's constructor should have two parameters:
  format: which is a string like "{0} of {1}" where {0} will be replaced by current page and {1} by the page count
  grid: which is a reference to the SPGridView

class XofYPager : ITemplate
{
    GridView _grid;
    String _format;
    public XofYPager(string format, GridView grid)
    {
        _format = format;
        _grid = grid;
    }

    public void InstantiateIn(Control container)
    {
        Table tbl = new Table();
        container.Controls.Add(tbl);
        tbl.Width = Unit.Percentage(100);
        TableRow row = new TableRow();
        tbl.Rows.Add(row);
        TableCell cell = new TableCell();
        row.Cells.Add(cell);
        cell.HorizontalAlign = HorizontalAlign.Center;
 
        int currentPage = _grid.PageIndex + 1;
        if (currentPage > 1)
        {
            ImageButton prevBtn = new ImageButton();
            prevBtn.ImageUrl = "~/_layouts/images/prev.gif";
            prevBtn.CommandName = "Page";
            prevBtn.CommandArgument = "Prev";
            cell.Controls.Add(prevBtn);
        }
        cell.Controls.Add(new LiteralControl(String.Format(_format, currentPage, _grid.PageCount)));
        if (currentPage < _grid.PageCount)
        {
            ImageButton nextBtn = new ImageButton();
            nextBtn.ImageUrl = "~/_layouts/images/next.gif";
            nextBtn.CommandName = "Page";
            nextBtn.CommandArgument = "Next";
            cell.Controls.Add(nextBtn);
        }
    }
}

 

The SmartPager's constructor should have two parameters:
  additionalpages: which is the number of pages to show on each side of the current page (should be >= 1, 2 is used in the example above)
  grid: which is a reference to the SPGridView

class SmartPager : ITemplate
{
    GridView _grid;
    int _additionalpages;

    public SmartPager(int additionalpages, GridView grid)
    {
        _additionalpages = additionalpages;
        _grid = grid;
    }

    public void InstantiateIn(Control container)
    {
        Table tbl = new Table();
        container.Controls.Add(tbl);
        tbl.Width = Unit.Percentage(100);
        TableRow row = new TableRow();
        tbl.Rows.Add(row);
        TableCell cell = new TableCell();
        row.Cells.Add(cell);
        cell.HorizontalAlign = HorizontalAlign.Center;

        int currentPage = _grid.PageIndex + 1;
        if (_grid.PageCount < (5+_additionalpages*2)+1)
        {
            AddPages(cell,1,_grid.PageCount,currentPage);
        }
        else
        {
            if (currentPage < 4+_additionalpages)
            {
                AddPages(cell,1,3+_additionalpages*2,currentPage);
                cell.Controls.Add(new LiteralControl("..."));
                AddPages(cell,_grid.PageCount,_grid.PageCount,currentPage);
            }
            else
            {
                AddPages(cell,1,1,currentPage);
                cell.Controls.Add(new LiteralControl("..."));
                if (currentPage>_grid.PageCount-3-_additionalpages)
                    AddPages(cell,_grid.PageCount-2-_additionalpages*2,_grid.PageCount,currentPage);
                else
                {
                    AddPages(cell,currentPage-_additionalpages,currentPage+_additionalpages,currentPage);
                    cell.Controls.Add(new LiteralControl("..."));
                    AddPages(cell,_grid.PageCount,_grid.PageCount,currentPage);
                }
            }
        }
    }
 
    void AddPages(Control container, int from, int to, int current)
    {
        for (int i = from; i <= to; i++)
        {
            LinkButton link=new LinkButton();
            link.CommandName="Page";
            link.CommandArgument=i.ToString();
            link.Text = i.ToString();
            if (i == current)
                link.Style.Add(HtmlTextWriterStyle.FontWeight,"700");
            container.Controls.Add(link);
            if (i<to)
                container.Controls.Add(new LiteralControl("|"));
        }
    }
}


Friday, December 19, 2008 10:16:11 PM (Romance Standard Time, UTC+01:00)  #    Comments [4]

Title: Caching a SharePoint list as a datatable


For one of my current projects I needed to show a SPGridView with data from a small list on the frontpage of the Intranet.

The list wasn't very small so didn't want to read if from the database on every hit of the frontpage, but on the hand it was so big that I couldn't cache it.

Now the big question was how to cache it.
If I used PortalSiteMapProvider then it'll always be refreshed when there was a change, but it would be hard to get the data into the SPGridView for displaying, sorting, paging and filtering.
If I on the other hand justed cached a DataTable, then it'll be easy to use in the SPGridView, but then I'd have to figure out when to invalidate the cache. It should be often enough that users wasn't annoyed with out of date data, but seldom enoughtthat it didn't annoy them due to the performance hit.

So I decided to combine the two and use the PortalSiteMapProvider to figure out when an update was needed and the DataTable for the real caching of the list items.

If you need something similar then here is my class, it doesn't deal with item level permissions, so if you need that you'll have to implement something different:

class DataTableCacheObject
{
    DateTime? lastModified;
    DataTable dataTable;

    public class Result
    {
        public DataTable dataTable { private set; get; }
        public SPList list { private set; get; }
        internal Result(DataTable dataTable, SPList list)
        {
            this.dataTable = dataTable;
            this.list = list;
        }
    }

    public static Result GetDataTable(string webUrl, string listName)
    {
        Result result = null;
 
        if (string.IsNullOrEmpty(listName))
            return null;

        Guid webId = new Guid();
        if (string.IsNullOrEmpty(webUrl))
            webId = SPContext.Current.Web.ID;
        else
        {
            using (SPWeb web = SPContext.Current.Site.AllWebs[webUrl])
            {
                webId = web.ID;
            }
        }

        SPSecurity.RunWithElevatedPrivileges(delegate()
        {
            using (SPSite site = new SPSite(SPContext.Current.Site.ID))
            {
                using (SPWeb web = site.AllWebs[webId])
                {
                    // Check if user has view rights to list
                    //
                    SPList list = web.Lists[listName];
                    if (!list.DoesUserHavePermissions(SPContext.Current.Web.CurrentUser,SPBasePermissions.ViewListItems))
                        return;
 
                    // Get Last Modified datetime
                    //
                    PortalSiteMapProvider psmp = PortalSiteMapProvider.GlobalNavSiteMapProvider;
                    PortalWebSiteMapNode node = psmp.FindSiteMapNode(web.ServerRelativeUrl) as PortalWebSiteMapNode;
                    SPQuery query = new SPQuery();
                    query.Query = "<OrderBy><FieldRef Ascending=\"FALSE\" Name=\"Modified\" /></OrderBy>";
                    query.RowLimit = 1;
                    SiteMapNodeCollection smnc = psmp.GetCachedListItemsByQuery(node, listName, query, web);
                    DateTime? lastModified = null;
                    foreach (SiteMapNode smn in smnc)
                    {
                        lastModified = (DateTime)((PortalListItemSiteMapNode)smn)[SPBuiltInFieldId.Modified];
                    }

                    // Look up DataTable in Cache
                    //
                    string cacheKey = String.Format("DataTable:{0}:{1}", web.ID, listName);
                    DataTableCacheObject dtco = HttpContext.Current.Cache[cacheKey] as DataTableCacheObject;
                    if (dtco != null)
                    {
                        // If not modified use DataTable from Cache
                        //
                        if (dtco.lastModified.Equals(lastModified))
                        {
                            result = new Result(dtco.dataTable.Copy(),list);
                            return;
                        }

                        // Modified => Replace
                        //
                        dtco.dataTable.Dispose();
                    }
 
                    // Get new DataTable and put into Cache
                    //
                    dtco = new DataTableCacheObject();
                    dtco.lastModified = lastModified;
                    dtco.dataTable = list.Items.GetDataTable();
                    HttpContext.Current.Cache[cacheKey] = dtco;
                    result = new Result(dtco.dataTable.Copy(), list);
                }
            }
        });
        return result;
    }
}


Friday, December 19, 2008 9:57:40 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Date: # Sunday, November 16, 2008

Title: WCMexport version 1.0


When you're branding a site using SharePoint 2007 Web Content Management, you really want to spend your time changing Master Pages and Page Layouts. But you often find that you spend a lot of your time editing your Feature.xml and different Elements.xml files especially if you have a lot of different Page Layouts with web parts for different parts of the site.
WCMexport is a small utility which can help you if you don't like doing all the editing of the xml-files from scratch.
With WCMexport you can use SharePoint Designer or what other tool you want to create the Master Pages, Page Layouts and Style Library Files on a development site. Once you're done and want to create your feature, you can just:

  1. Start WCMexport
  2. Enter the url of you site
  3. Press enter or click "Get Content"
  4. Select the files you want included in your Feature
  5. Click "Export"
  6. Select the location and name of your Feature.xml file
WCMexport will then create your Feature.xml, "Elements".xml files and sub-directories in the location you specified for your Feature.xml file.
There is a few things WCMexport will do that you might not expect:
  • It uses the SharePoint object model so it needs to run on the server
  • If your filename or description starts with an _ (underscore) WCMexport will remove it. (This enables you to add your generated feature back to the same site for testing if you just get into the habit or adding the _ when you create files using SPD)
  • If multiple Page Layouts has the same HTML then WCMexport will only create one file and have the other Page Layouts reuse this file (This is helpful if you have multiple Page Layouts which only differ in which content type they support or which web-parts are present)
  • Preview Images are put into "_catalog/masterpage/Preview Images" regardless of where you have them on your source site

You may still have to edit the generated files to add more content and make your Manifest.xml and .ddf file, but my hope is that WCMexport still will be a great help for you in your WCM development.
(You may use AC's WCM Custom Commands for STSADM.EXE to export Site Columns and Content types and WSPBuilder to generate Manifest.xml and .ddf file)
Try WCMexport and let me know what you think.
What should be in 2.0?
You find a zip file with WCMexport 1.0 here and the source code here.



Sunday, November 16, 2008 1:52:46 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Date: # Thursday, August 21, 2008

Title: Turn Default Upload Overwrite Off in WSS 3.0


By default the Overwrite checkbox in Upload.aspx is checked in WSS 3.0 and MOSS 2007, but sometimes you may want to change this default.

MOSS junkie has a post on how to change that default.
But his solution has the downside of having to modify one of the standard application pages which is unsupported and may be overwritten by a upgrade

So how do we change the default without modifying upload.aspx?

Once again the <delegate> controls in the default masterpage can help us.
This time it's the AdditionalPageHead delegate we can use. This delegate allows you to add any number of usercontrols to the page header of every page in sites where your feature is activated.
So we add a feature with the following elements.xml file:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <Control Id="AdditionalPageHead"
           Sequence="100"
           ControlSrc="~/_controltemplates/DefaultUploadOverwriteOff/DefaultUploadOverwriteOff.ascx" />
</Elements>


The usercontrol we add is very simple it just add a javascript file to our pages:

<%@ Control Language="C#" %>
<script src="/_controltemplates/DefaultUploadOverwriteOff/DefaultUploadOverwriteOff.js"
        language="javascript"
        type="text/javascript">
</script>


Even the javascript file is very simple, it just loops throught all input-control on the page and if it's the OverwriteSingle or OverwriteMultiple checkbox it unchecks it:

function DefaultUploadOverwriteOff()
{
  var inputs = document.getElementsByTagName('input');
  var i=0;
  for (i=0;i<inputs.length;i++)
  {
    var input = inputs[i];
    if (input.type == 'checkbox' 
     && (input.id.indexOf('OverwriteSingle')>-1
      || input.id.indexOf('OverwriteMultiple')>-1))
      input.checked = false;
  }
}
_spBodyOnLoadFunctionNames.push('DefaultUploadOverwriteOff');


All the source is here and if you just want a solution file it's here



Thursday, August 21, 2008 11:52:41 AM (Romance Standard Time, UTC+01:00)  #    Comments [5]

Date: # Friday, August 15, 2008

Title: Remove the Search Box from SharePoint Sites


Sometimes you don't want your users to be able to search on your SharePoint site.
Part one of this is then to remove your site from the content source in SSP, but you probably also want to remove the Search box from every page in your site.

Fortunately SharePoint makes this very easy

The different version of SharePoint has different requirements for the Search box and the way these different serach boxes are implemented are using the Delegate feature of WSS
Delegates are controls in a masterpage/page whose content is determined by which Features are activated.
Each Delegate has an ID (SmallSearchInutBox for the Search box) and each Feature can vote for it's content by specifying an Control element.
For Delegates where AllowMultipleControls are false (like SmallSearchInputBox) the Control with the lowest sequence wins (25 is the lowest sequence used by OOB Feature)
So to disable the Search box all you have to do is implement a Feature which has a Control element with sequence below 25 pointing to an empty UserControls.
If you don't want to develop this yourselves you can just download my implementation which makes a Feature with WebApplication scope.
The source files are here and a precooked solution file is here



Friday, August 15, 2008 8:38:30 AM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Date: # Wednesday, July 16, 2008

Title: Remove Login box when anonymous users download office document from SharePoint Site


When developing Extranet/Internet site in SharePoint you often want to allow anonymous access and this works fairly well.
But there is one are where the out of the box experience fails regarding anonymous access and that is when you allow the users to download Microsoft Office documents. In that case IE/Office pops up a couple of Login dialogs, if the user cancels out of these the document opens as expected, but you really don't want the user to have to cancel a couple of dialogs to open your documents

The problem is that office tries to be intelligent and issues a Microsoft Office Protocol Discovery request to see how much the user is allowed to do, but SharePoint responds with access denied until the users logs in.

The solution I've found is to implement a HttpModule which rejects the Microsoft Office Protocol Discovery request if the user isn't logged in and this gets rid of the Login boxes
The essential code is fairly simple:

void context_PostAuthenticateRequest(object sender, EventArgs e)
{
    HttpApplication app=sender as HttpApplication;
    if (app.User.Identity.IsAuthenticated)
        return;

    HttpRequest request = app.Request;
    if (request.UserAgent != "Microsoft Office Protocol Discovery"
     && request.UserAgent != "Microsoft Data Access Internet Publishing Provider Protocol Discovery"
     && request.UserAgent != "MSFrontPage/12.0")
        return;

    HttpResponse response = app.Response;
    response.StatusCode = 404;
    response.End();
}

This Zip file contains both the source code and a compiled dll.
You can just add the dll to the GAC and add the following line as the first httpModule inside <httpModules> after the <clear /> element in web.config:
<add name="RemoveMicrosoftOfficeProtocolDiscoveryLoginBoxModule" type="TheBlackKnightSings.RemoveMicrosoftOfficeProtocolDiscoveryLoginBoxModule, RemoveMicrosoftOfficeProtocolDiscoveryLoginBoxModule, Version=1.0.0.0, Culture=neutral, PublicKeyToken=1537c6b9f16ad88e" />
This is only tested with Office 2007, you may have to turn off Client Integration for it to work with all version of Office



Wednesday, July 16, 2008 5:33:49 PM (Romance Standard Time, UTC+01:00)  #    Comments [27]

Date: # Monday, July 14, 2008

Title: FeatureReceiver To Cleanup WebPart Files


Many web-part developers are surprised to see that their web-parts are listed as available even though they have deactivated the feature which contains them

And the users are equally surprised when they then try to add the web-part only to see it fail because the code (and safe-control entry) has been removed.

The problem comes from the fact that deactivating a Feature doesn't remove files provisioned using Module and File entries

The solution is quiet simple:
Just make a feature receiver which deletes all the .webpart files in the FeatureDeactivating event. You can even get all the files to delete from the xml-files associated with the feature so the following code does it all automatically:

public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
    using (SPSite site = properties.Feature.Parent as SPSite)
    {
        RemoveWebParts(site, properties.Definition);
    }
}

private void RemoveWebParts(SPSite site, SPFeatureDefinition definition)
{
    using (SPWeb web = site.RootWeb)
    {
        string wpcatalogUrl = SPUrlUtility.CombineUrl(web.Url, "_catalogs/wp");

        // Get the Feature.xml for the feature
        //
        XmlDocument featureXml = new XmlDocument();
        featureXml.LoadXml(definition.GetXmlDefinition(web.Locale).OuterXml);

        XmlNamespaceManager nsMgr = new XmlNamespaceManager(featureXml.NameTable);
        nsMgr.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/");

        // Get the Location attribute of each ElementManifest inside each ElementManifests inside the Feature
        //
        foreach (XmlNode locationNode in featureXml.SelectNodes("/sp:Feature/sp:ElementManifests/sp:ElementManifest/@Location", nsMgr))
        {
            // Get the ElementManifest XML
            //
            XmlDocument elementManifest = new XmlDocument();
            elementManifest.Load(Path.Combine(definition.RootDirectory, locationNode.Value));
            XmlNamespaceManager nsMgr2 = new XmlNamespaceManager(elementManifest.NameTable);
            nsMgr2.AddNamespace("sp", "http://schemas.microsoft.com/sharepoint/");

            // Get the Url attribute of each file in Modules (with List attribute='113' WebPartCatalog
            //
            foreach (XmlNode webPartFileUrl in elementManifest.SelectNodes("/sp:Elements/sp:Module[@List='113']/sp:File/@Url", nsMgr2))
            {
                // Delete the file from the WebPart Catalog
                //
                SPFile wpFile = web.GetFile(SPUrlUtility.CombineUrl(wpcatalogUrl,webPartFileUrl.Value));
                wpFile.Delete();
            }
 
        }
    }
}

And if you combine this with the code from this post then you get a file like this which works even if the Feature is removed as part of a solution being retracted.



Monday, July 14, 2008 6:02:34 PM (Romance Standard Time, UTC+01:00)  #    Comments [0]

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way. And all information or programs are without warranty. Use at your own risk


© Copyright 2013 Send mail to the author(s) Per Jakobsen Feed your aggregator (RSS 2.0)