Showing posts with label Sharepoint 2007. Show all posts
Showing posts with label Sharepoint 2007. Show all posts

26/02/2010

SharePoint Folder Icon Changer JSON MetaData Provider Control

This is a follow up to my original post on How To Change Folder Icons in SharePoint MOSS Using JavaScript.

I'd encourage you to read that post first so you have at least a basic understanding of the JavaScript involved in actually changing the folder icons.

The .Net Custom Control is much simpler. Here is the complete code C# file fully commented:

using System;
using System.Collections.Generic;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using EventPhotoGallery.Data;
using Microsoft.SharePoint;

namespace EventPhotoGallery
{

// Create a sub control object we can use on our ASPX page to define content type to icon mappings
[AspNetHostingPermission(System.Security.Permissions.SecurityAction.Demand,
Level = AspNetHostingPermissionLevel.Minimal)]
public class IconMapping : WebControl
{

public string ContentTypeName { get; set; }

public string IconUrl { get; set; }

public string ThumbnailUrl { get; set; }

public string ToJSON()
{
return string.Format("'{0}': {{ ContentTypeName: '{0}', IconUrl: '{1}', ThumbnailUrl: '{2}' }}",
ContentTypeName.QuoteEscape(), IconUrl.QuoteEscape(), ThumbnailUrl.QuoteEscape());
}

}

// Create The master web control which compiles the ASPX arguments and registers a script
[ParseChildren(true, "IconMappings")]
[ToolboxData("<{0}:FolderContentTypeIconChanger runat=server>")]
public class FolderContentTypeIconChanger : WebControl, INamingContainer
{
// Define a key to register the MetaData JSON object with the .Net Page Rendering process
private const string FOLDERTYPESCRIPTKEY = "FolderContentTypeIconChanger";

// A list of our MetaData Controls defined above that will be subcontrols in ASPX
private List _IconMappings = new List();

public List IconMappings { get { return _IconMappings; } set { _IconMappings = value; } }

public string ListName { get; set; }

public string RootFolder { get; set; }

protected override void OnInit(EventArgs e)
{
base.OnInit(e);
// when the AllItems.aspx page loads the current folder is always in the QueryString so let's grab it
if (!string.IsNullOrEmpty(Page.Request.QueryString["RootFolder"]))
RootFolder = Page.Request.QueryString["RootFolder"];
}

protected override void OnPreRender(EventArgs e)
{
// During prerendering let's build the JSON structure that the _AllItems object consumes.
if (!Page.ClientScript.IsStartupScriptRegistered(GetType(), FOLDERTYPESCRIPTKEY))
Page.ClientScript.RegisterStartupScript(GetType(),
FOLDERTYPESCRIPTKEY, CreateScript(), true);

base.OnPreRender(e);
}

protected override void Render(HtmlTextWriter writer)
{
// Do nothing with the rendering cancel it - we don't need any html with this control the
// JavaScript has already been registered
//base.Render(writer);
}

// The worker horse - does all the work compiling the MetaData
private string CreateScript()
{
SPFolder folder;

// if there is no folder defined then we should use the root folder from the current context list
if (!string.IsNullOrEmpty(RootFolder))
folder = SPContext.Current.Site.RootWeb.GetFolder(RootFolder);
else
folder = SPContext.Current.Web.Lists[ListName].RootFolder;

// Get the SharePoint ContentType for the folder
string folderContentType = folder.Item != null ? (folder.Item.GetString(PhotoData.CONTENTTYPE) != null ?
folder.Item.GetString(PhotoData.CONTENTTYPE) : "Folder") : "Folder";

// Create a StringBuilder into which we will be appending JSON atoms
StringBuilder builder = new StringBuilder("var _FolderContentTypes = {");

// this is a custom data tier object which you're going to have to figure out on
// your own - if there is enough demand I can write another article with a
// comprehensive pattern to support folders in your document libraries
EPGData data = new EPGData();

// get all subfolders and list in json to the UI.
if (folder != null && folder.SubFolders.Count > 0)
{
// iterate all subfolders appending JSON Atoms to the script
foreach(SPFolder subFolder in folder.SubFolders)
builder.AppendLine(CreateFolderReference(subFolder.ServerRelativeUrl,
data.Folder.GetContentTypeName(subFolder), subFolder.Name));
// data.Folder.GetContentTypeName(subFolder) merely returns the name of the content type
builder.Remove(builder.Length - 3, 3); // remove the last comma
}

builder.AppendLine(string.Format("}};\n_FolderContentTypes.RootUrl = '{0}';", SPContext.Current.Site.Url.QuoteEscape()));
// QuoteEscape is an extension method which escapes all single quotes so the JavaScript is compliant

// add the icon mappings to the control
if (IconMappings.Count > 0)
{
builder.AppendLine("var _FolderContentTypeIconMaps = {");
foreach (IconMapping mapping in IconMappings)
{
builder.AppendLine(mapping.ToJSON() + ",");
}
builder.Remove(builder.Length-3,3).AppendLine("};");
}

builder.AppendLine(string.Format("\nvar _CurrentFolder = {{ Name: '{0}', ContentTypeName: '{1}' }};", folder.Name.QuoteEscape(), folderContentType.QuoteEscape()));

return builder.ToString();

}

private string CreateFolderReference(string folderUrl, string contentTypeName, string folderName)
{
return string.Format("'{0}':{{FolderName: '{2}', FolderUrl:'{0}', ContentType: '{1}'}},",
folderUrl.QuoteEscape(), contentTypeName.QuoteEscape(), folderName.QuoteEscape());
}


}

}


So there are a couple things you still have to figure out in your own implementation, but the above is a good starting point.

Now how do you add this to the page you may say - well - first, add the assembly to the GAC.

Then within the appropriate Template Folder find your AllItems.aspx page and add a directive to register the assembly as such:

<%@ Register TagPrefix="my" Namespace="EventPhotoGallery" Assembly="EventPhotoGallery, Version=1.0.0.0, Culture=neutral, PublicKeyToken=9f4da00116c38ec5" %>

Next, somewhere in your ASPX code you can now add the JSON FolderContentTypeIconChanger control as such:

<my:FolderContentTypeIconChanger id="folderTypes" runat="server"
ListName="Event Photo Gallery Photographs">

<my:IconMapping ContentTypeName="Location Folder"
IconUrl="/_layouts/EventPhotoGallery/images/Location_Folder-16x16.png"
ThumbnailUrl="/_layouts/EventPhotoGallery/images/Location_Folder-48x48.png" />

<my:IconMapping ContentTypeName="Event Photo Folder"
IconUrl="/_layouts/EventPhotoGallery/images/Photo_folder-16x16.png"
ThumbnailUrl="/_layouts/EventPhotoGallery/images/Photo_folder-48x48.png" />

<my:IconMapping ContentTypeName="User Upload Folder"
IconUrl="/_layouts/EventPhotoGallery/images/Upload_Folder-16x16.png"
ThumbnailUrl="/_layouts/EventPhotoGallery/images/Upload_Folder-48x48.png" />

</my:FolderContentTypeIconChanger>

I'm sure you can imagine what means what etc. but ya - this should get you going in adding custom icons to your SharePoint Folders.

Let me know if you have any other questions!

Over and Out

Change Folder Icons In MOSS / SharePoint Using JavaScript

I hope you guys find this code useful in changing default folder icons using JavaScript.

Before that - I want to point out that many people are of the opinion that Folders should be avoided at all costs in SharePoint. While I appreciate the perspective there, I believe it merely shows there is a gap in the technology platform.

I believe that once that gap is filled (EX// through CustomActions, EventReciever Methods) they actually become a pretty valuable resource in allowing old school business users to transition from windows based folder system - to a web based folder system - which opens them up to the more modern ways of locating resources VIA search / taxanomy. Moreover, how many SharePoint business developers out there haven't had a client ask for Folders?

OK - to the JavaScript. I want to start off by pointing out that this entire methodolgy will most likely be ineffective if you've customized the List Views (if the HTML structure is different the JavaScript below will not be able to find the correct HTML elements to change).

Before you start - you're going to need to grab a copy of this cross browser getElementsByClassName method and add it to your script (It's tha bomb if your architecture prohibits the use of jQuery :().

First, I create a couple JSON objects to hold Meta Data about my ContentTypes / Folders (I actually use a .Net Custom Control to auto generate this dynamically when the View loads - check here for that code):

// This data structure contains information about the folders at this particular level
// in the folder hierarchy within the current AllItems view
var _FolderContentTypes = {

// Key the list By Folder URL as we'll use it as a Lookup later
'/Lists/EventPhotoGalleryPhotographs/User Uploads':

{
// All metadata related to above folder
FolderName: 'User Uploads',
FolderUrl:'/Lists/EventPhotoGalleryPhotographs/User Uploads',
ContentType: 'User Upload Folder'

},

// next folder
'/Lists/EventPhotoGalleryPhotographs/Forms':

{
// more metadata
FolderName: 'Forms',
FolderUrl:'/Lists/EventPhotoGalleryPhotographs/Forms',
ContentType: 'Folder'

}

};

// next we'll setup the root url to use as a reference
_FolderContentTypes.RootUrl = 'http://mtw-sharepoint';

// The second data structure we create maps ContentTypes to Icon Urls as such
var _FolderContentTypeIconMaps = {

// The SharePoint ContentType 'Location Folder'
'Location Folder':
{
ContentTypeName: 'Location Folder',
// icon to use in details view
IconUrl: '/_layouts/EventPhotoGallery/images/Location_Folder-16x16.png',
// icon to use in thumbnails view
ThumbnailUrl: '/_layouts/EventPhotoGallery/images/Location_Folder-48x48.png'
},

// Another SharePoint Folder ContentType
'Event Photo Folder':
{

// more meta data
ContentTypeName: 'Event Photo Folder',
IconUrl: '/_layouts/EventPhotoGallery/images/Photo_folder-16x16.png',
ThumbnailUrl: '/_layouts/EventPhotoGallery/images/Photo_folder-48x48.png'

},

// Another SharePoint ContentType etc...
'User Upload Folder': {

ContentTypeName: 'User Upload Folder',
IconUrl: '/_layouts/EventPhotoGallery/images/Upload_Folder-16x16.png',
ThumbnailUrl: '/_layouts/EventPhotoGallery/images/Upload_Folder-48x48.png'

}

};


Next I'll create a worker object I can use in my _AllItems view that actually does the dirty work. This can be saved in a JavaScript file / cached in the client browser:

var _AllItems = {

// create the initialization method which will execute when the window has loaded
Initialize: function() {

// This section sets the small icons when in details view
var folders = getElementsByClassName("ms-vb-icon");

// if there are no folders then don't try changing icons
if (folders.length > 0) { // this method works when we are in details view

// define a constant to recognize if we are in the root folder
var rf = "RootFolder=";

// loop through each HTMLElement that was returned with the ms-vb-icon class selector
for (var intCnt = 0; intCnt < folders.length; intCnt++) {

// perform some string parsing in order to figure out the URL of the folder as

// this is what we use as our Key in determining which Icon to apply to the folder var startPos = folders[intCnt].firstChild.search.indexOf(rf) + rf.length;
var endPos = folders[intCnt].firstChild.search.indexOf("&", folders[intCnt].firstChild.search.indexOf(rf) + rf.length + 1);

// get the folder URL and replace all SharePoint Url Hexidecimal's to get a pure Url reference var listUrl = folders[intCnt].firstChild.search.substr(startPos, endPos - startPos);
listUrl = listUrl.replace(/%2f/g, "/").replace(/%20/g, " ");

// double check to make sure this particular folders' URL exists in our _FolderContentTypes
// meta data object
if (_FolderContentTypes[listUrl] != null) {
// double check that our _FolderContentTypeIconMaps data structure contains the ContentType
// for which we want to apply a custom icon to
if (_FolderContentTypeIconMaps[_FolderContentTypes[listUrl].ContentType] != null) {

// now actually change the icon url to the new URL we have specified in our iconmap folders[intCnt].firstChild.firstChild.src = _FolderContentTypes.RootUrl + _FolderContentTypeIconMaps[_FolderContentTypes[listUrl].ContentType].IconUrl; }

}

}

}

// The following section repeats the above methodology for the _AllItems
// thumbnail view using larger custom icons folders = getElementsByClassName("thumbnail"); if (folders.length > 0) {

// this method works when we are in thumbnail view
for (var intCnt = 0; intCnt < folders.length; intCnt++) {

// you can see just how sketchy this method is - I surround it in a try catch blatantly
// because I'm too lazy to code the massive statement required to make sure each
// element exists programatically try { var imgObj = folders[intCnt].firstChild.firstChild
.firstChild.firstChild.firstChild.firstChild
.firstChild.firstChild.firstChild.firstChild; } catch (ex) { }

// The difference here is we'll see every thumbnail icon on the page so we have // to double check the icon is actually the default SharePoint folder icon
if (imgObj != null && imgObj.src.indexOf("fldrnew.gif") > -1) {
var folder =
this.GetFolderByName(folders[intCnt].firstChild.firstChild.children[1].firstChild.innerText);

// Again, ensure the icon map contains an icon for this folders content type
if (_FolderContentTypeIconMaps[folder.ContentType] != null) {

// Actually change the thumbnail icon source
imgObj.src = _FolderContentTypes.RootUrl + _FolderContentTypeIconMaps[folder.ContentType].ThumbnailUrl;

// The icons I used were a specific constant size so I adjusted the style props as required
imgObj.style.width = "auto";
imgObj.style.height = "48px";
imgObj.title = folder.ContentType;

}

}

imgObj = null; // Just kill the obj ref

}

}

},

// This method gets the real folder name
GetFolderByName: function(folderName) {
if (folderName != null) {
folderName = folderName.replace(/^\s\s*/, '').replace(/\s\s*$/, '')
for (var f in _FolderContentTypes) {
if (_FolderContentTypes[f].FolderName == folderName)
return _FolderContentTypes[f];
}
}
},
// This is the startup method that should be called when the window loads
StartUp: function() {
var ol = window.onload;
if (ol != null) {
window.onload = function(e) {
ol(e);
_AllItems.Initialize();
}
} else {
window.onload = function() { _AllItemsInit.ialize(); }
}
}
};

// Wire up object initialization
_AllItems.StartUp();

And that's about it. When the window has completed loading _AllItems.Initialize() is called which changes the src property on all SharePoint icons. With the metadata provided the above example changes one folders' icons - the "User Upload" ContentType folder.

Obviously the above could be greatly simplified using jQuery - you could probably do it in about 15 lines (I'd encourage someone to do that and flip a trackback below).

Blah Blah Blah - The .Net Control which produces the JSON metadata

09/11/2009

Uri Extension Method: AbsolutePathNoFile()

How often do you want to determine the absolute URI from any url without the specific file reference and without the query string variables. This is especially useful in many SharePoint tasks.

Here is a little extension method that can do just that. Pop this in a static class and try calling it on your Uri object:

public static UriExtensions {

public static string AbsolutePathNoFile(this Uri theUri)
{
string retVal = "http://" + theUri.Host + (theUri.Port == 80 ? "" : ":" + theUri.Port.ToString());
for (int intCnt = 0; intCnt < theUri.Segments.Length-1; intCnt++) {
retVal += theUri.Segments[intCnt];
}
return retVal;
}
}

// somewhere else:

Uri u = new Uri(Request.Url);
MessageBox.Show(u.AbsolutePathNoFile());

Over and Out

06/10/2009

SharePoint Tips: Content Type Visibility on NewMenu

I've built a solution with a few custom ContentTypes in a list definition. Now, removing these from the NewMenu has to be easy right? Just add a HideCustomAction definition to your instance file. Not so easy to find the ID's that will be used for those content types is it? Well, here is a solution that will remove the ContentTypes from the NewMenu but allow them to hang around for use in defining whatever it is your heart desires.

To specify visibility on the Microsoft.SharePoint.StandardMenu.NewMenu in a document library, try changing the RequiredClientRenderingOnNew option to false, and update the content type definition as below:

The correct solution for document libraries is the following:

SPWeb web = SPContext.Current.Site.RootWeb;
web.AllowUnsafeUpdates = true;// if done on get
SPList list = web.Lists["LIST NAME LIST NAME PUT LIST NAME OR GUID HERE"];

foreach (SPContentType ct in list.ContentTypes)
{
if (ct.Name == "Content Type Name")
{
ct.Hidden = true;
ct.RequireClientRenderingOnNew = false;
ct.Update();
}
}
web.AllowUnsafeUpdates = false;// if done on get

I'm doing this in a feature receiver and it works by design. If done in AllItems.aspx (or any GET request it seems) you need to refresh the page twice for this to work by design (one refresh appears to set the value in content type but this is done after it's read into the display).

The other thing I've got in that habit of doing is force deleting lists when I make changes. I'm not sure how "required" this is but in order to get stability in my test results this seems to make a difference.

Here is how I was force deleting the offending lists:

stsadm -o forcedeletelist -url http://SERVERNAME:PORT/Lists/LISTNAMELISTURLNAMELISTURLNAME


Wish SharePoint would work as advertised ... sigh ...

14/08/2009

Clear | Remove Query Strings From Post Backs (ASP.Net, SharePoint)

As part of an unorthodox request I was asked to make a TabStrip component SEO friendly. My first thought was to completely rewrite the control to make use of GET requests rather than continuing to POST the data back to the server.

Immediately I ran into a problem with this approach as the developers of this site have implemented the majority of these TabStrip controls inside the ASP.Net UpdatePanel (don't get me started with these) which does not recognize GET requests as Async events.

The architect of this site (actually a SharePoint site) failed to recognize that the SharePoint indexing service would not be able to index information on any tab other than the default one (as subsequent requests for tab data are done using Asynchronous Events). When project management asked him to modify the tabs for accessibility he told them it was impossible without a complete redesign.

As a solution to this problem I modified the TabStrip control such that it renders an anchor tag rather than a link button and added some fancy JavaScript to cancel the Anchor tag click event and trigger the appropriate __doPostBack option. I now have a crawl able anchor tag, which works like a link button, and triggers the appropriate update.

Moreover, when the TabStrip is initialized (OnInit) it looks for a specific query string variable indicating the ID of the selected tab. This is when the real problem starts:
  1. Indexing service crawls the anchor and builds a GET string reference to the selected tab id.
  2. Page is loaded into browser.
  3. User selects different tab.
  4. TabStrip is initialized finding the previous (last selected tab) tab id.
  5. TabStrip raises MenuItem Click event ignoring the actual new selected tab id.
The problem then becomes: How do I Clear The Query String From Subsequent Post Back Operations?

My quest turned to the Google.

First I found this approach which actually removes the query string VIA triggering a post back and is therefore too wasteful to be my solution.

I then found the following approach which unfortunately fails to load as it uses the ugly document.all JavaScript method which isn't available until after page load (therefore causes a client side exception - even when registered properly as a StartupScript).

I put together a slightly different JavaScript routine that looks like this:

if(window.onload != null) {
var wol = window.onload;
window.onload = function(e) {
wol(e);
document.forms[0].action = "Index.aspx";
}
} else {
window.onload = function() {
document.forms[0].action = "Index.aspx";
}
}


This JavaScript Method then rolls nicely into an ASP.Net Server Control like so:
///
/// Attempt to remove the tab id from the query string
///

private void AddRemoveTabIdQueryString()
{
const string key = "REMOVETABIDQUERYSTRING";
if (!Page.ClientScript.IsStartupScriptRegistered(GetType(), key))
{
Page.ClientScript.RegisterStartupScript(this.GetType(), key,
string.Format("if(window.onload != null) {{var wol = window.onload;window.onload = function(e) {{wol(e);{0};}}}} else {{window.onload = function() {{document.forms[0].action = \"{0}\";;}}}}", GetRequestPageName()), true);
}

// Get the page Name of the request
private string GetRequestPageName()
{
string rawUrl = Page.Request.RawUrl;
int pos = rawUrl.LastIndexOf("/");
int qpos = rawUrl.LastIndexOf("?");
return qpos > -1 ? rawUrl.Substring(pos + 1, qpos - pos - 1) :
rawUrl.Substring(pos + 1, rawUrl.Length - pos - 1);
}

I hope this is helpful to others.

Over and Out

13/08/2009

SPJobDefinition Quirks For Developers

I wanted to write a quick heads up on a couple quirks relating to the SPJobDefinition and wiring up Timer Jobs (may be relevant for deployments where the Timer Jobs are modified). We are building our solutions on Windows 2003 Server with MOSS 2007.

To debug SPJobDefinitions attach VS debugger to OWSTIMER.exe process after following install instructions below.

OWSTIMER is caching the SPJobDefinition somewhere and is tricking Visual Studio into thinking we are actually debugging the most recent compiled version when we are debugging the last loaded version.

To fix this, kill the OWSTIMER process in process explorer (or restart the Windows Sharepoint Services Timer by typing the following at the command prompt: net stop sptimerv3 & net start sptimerv3) and then refresh the portal you are performing development on (this will force a restart of OWSTIMER.EXE and clear the cache).

Between tests ensure you completely remove the SharePoint feature (deactivate + uninstall) from Sharepoint (see below). Then kill the OWSTIMER process, iisreset, rebuild the solution and reinstall / activate the solution again (see below).

Here are a couple batch files convenient for removing / installing the jobs (where ProvisioningVX is the name of the feature you are trying to install):

Uninstall Batch File

@ECHO OFF

set SPAdminTool=%CommonProgramFiles%\Microsoft Shared\web server extensions\12\BIN\stsadm.exe
set TargetUrl=http://servername/sites/name

echo "Deactivating Feature"
"%SPAdminTool%" -o deactivatefeature -name ProvisioningVX -url %TargetUrl% -force

echo "Uninstalling Feature"
"%SPAdminTool%" -o uninstallfeature -name ProvisioningVX -force

iisreset

PAUSE

Installing and activating the feature

@ECHO OFF

set SPAdminTool=%CommonProgramFiles%\Microsoft Shared\web server extensions\12\BIN\stsadm.exe
set TargetUrl=http://servername/sites/name

echo "installing feature"
"%SPAdminTool%" -o installfeature -name ProvisioningVX -force

echo "activating feature"
"%SPAdminTool%" -o activatefeature -name ProvisioningVX -url %TargetUrl% -force

iisreset

PAUSE

12/08/2009

How To: Debug SPJobDefinition in Visual Studio

The solution is remarkably simple.

Click Debug -> Attach To Process

Note: At this point, ensure the checkbox "Select processes from all users" is selected.

Select OWSTIMER.EXE from the list.

Setup a breakpoint on the Execute override method.

Be patient and wait for the timed job to execute.

07/12/2007

Enterprise 2.0

Instead of building one system that tries to do everything, build a system that is capable of integrating with everything. From an IT perspective, Enterprise 2.0 is about gaining network effects from various systems, rather than choosing one limited strategy that is supposed to be all things to all people.
- Sharepoint 2007 - http://innovationcreators.com/wp/?p=271

Do you agree?

Over and Out