Feeds:
Posts
Comments

Archive for the ‘Sharepoint’ Category

As part of a Sharepoint solution, I had the need to remove the default Quick Launch entries (Lists, Libraries, Discussions), and insert my own sections with links to specific lists and pages.

The following method gets it done:

public static void CreateSideNav(string siteName, string webName)
{
	using (var spSite = new SPSite(siteName))
	{
		using (SPWeb spWeb = spSite.OpenWeb(webName))
		{
			try
			{
				SPNavigationNodeCollection leftNav = spWeb.Navigation.QuickLaunch;
				var nodesToDelete = new List<int>();

				for (int i = 0; i < leftNav.Count; i++)
				{
					if (leftNav[i] != null)
					{
						SPNavigationNode leftNavNode = leftNav[i];
						if (DefaultQuickLaunchEntriesToDelete.Contains(leftNavNode.Title))
							nodesToDelete.Add(i);
					}
				}

				nodesToDelete.Sort();
				nodesToDelete.Reverse();
				nodesToDelete.ForEach(x => leftNav[x].Delete());

				spWeb.Update();

				leftNav = spWeb.Navigation.QuickLaunch;

				// Add section headings
				var reportsNode = new SPNavigationNode("Reports", spWeb.Url, true);
				leftNav.AddAsFirst(reportsNode);

				var dataListsNode = new SPNavigationNode("Data Lists", spWeb.Url, true);
				leftNav.AddAsFirst(dataListsNode);

				var mainListsNode = new SPNavigationNode("Primary Lists", spWeb.Url, true);
				leftNav.AddAsFirst(mainListsNode);


				// Add individual links
				SPNavigationNode node;

				// MainListsNode
				node = new SPNavigationNode("Committees", string.Format("{0}/Lists/Committees", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Members", string.Format("{0}/Lists/Members", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Services", string.Format("{0}/Lists/Services", spWeb.Url), true);
				mainListsNode.Children.AddAsLast(node);

				// DataListsNode
				node = new SPNavigationNode("Committee Types", string.Format("{0}/Lists/CommitteeTypes", spWeb.Url), true);
				dataListsNode.Children.AddAsLast(node);

				node = new SPNavigationNode("Jurisdictions", string.Format("{0}/Lists/Jurisdictions", spWeb.Url), true);
				dataListsNode.Children.AddAsLast(node);

				spWeb.Update();
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.ToString());
			}
		}
	}
}

I then reference this from a feature receiver in my package, and pass in the specific site and web names. I also define a List of Quick Launch entries to remove like so:

static List<string> DefaultQuickLaunchEntriesToDelete { get { return new List<string>() { "Lists", "Libraries", "Discussions" }; } }

With this we will, from the specified web, remove those default entries (of course you can specify any you wish) and insert our own.

Read Full Post »

I really don’t like having to work with the ClientId of page elements in ASP.Net. You know, you give a textbox an ID of, say, “myTextbox” and then try to get a reference to it client side in Javascript and it fails. You try (with jQuery, say) to run:

$('#myTextbox').attr('value', 'BLAH!')

and it returns null. You inspect your generated html and find that your textbox has an ID of something like: ctl00_m_g_3bd415ee_814a_46e1_a0f1_a2cf8227bea0_ctl00_myTextbox

I understand why Microsoft molests the IDs that I assign to it – it’s making sure that (as dictated by W3c) IDs of elements on my page are unique, and it does this by assigning a GUID to my webpart, and then appending to it with the details and IDs of my individual controls.

But I want to (relatively easily) get a reference to the webpart GUID so that, in my webparts, I can manipulate my DOM elements client side (in Javascript).

So what I do (glad to hear if there are better approaches!) is:

1. Create and Implement an Interface to Expose the ClientId

I create an interface for my Visual Web Parts called IVisualWebPart that my User Controls implement. It looks (so far) like:

using System.Web.UI.WebControls.WebParts;

namespace MyNamespace.Interfaces
{
public interface IVisualWebPart
  {
    WebPartManager WebPartManager { get; set; }
    string ClientId { get; set; }
  }
}

I don’t actually use the WebPartManager in this example, but I use it in other cases, so I’ve just left it in here.

And then in the usercontrol itself:

#region IVisualWebPart members

WebPartManager webPartManager { get; set; }
public WebPartManager WebPartManager
{
get
{
if (webPartManager == null)
throw new NullReferenceException("The instantiating web part must assign WebPartManager to this UserControl.");

return webPartManager;
}
set
{
webPartManager = value;
}
}

string clientId { get; set; }
public string ClientId
{
get
{
if (clientId == null)
throw new NullReferenceException("The instantiating web part must assign ClientId to this UserControl.");

return clientId;
}
set
{
clientId = value;
}
}

#endregion

And in the WebPart code (when you create a Visual WebPart in Sharepoint, it generates a WebPart as well as a UserControl with code behind, and sets the WebPart to fetch and display the UserControl):

protected override void CreateChildControls()
{
Control control = Page.LoadControl(_ascxPath);
(control as IVisualWebPart).WebPartManager = this.WebPartManager;
(control as IVisualWebPart).ClientId = this.ClientID;
Controls.Add(control);
}

2. Expose the ClientId via Code

I can then expose the ID on the page by injecting some JS onto the page from code behind like this:

private void AddClientIdJs()
{
StringBuilder js = new StringBuilder();
js.Append("<script type='text/javascript'>");
js.Append("$(function () {");
js.Append("var thisClientId = \"" + this.clientId + "\";");
js.Append("DispForm_Committee_RelatedAppointmentsUserControl_startUp(thisClientId);");
js.Append("});");
js.Append("</script>");
this.Page.Header.Controls.Add(new LiteralControl(js.ToString()));
}

I call this method from an overridden “CreateChildControls()”.

You’ll note that I’m actually using the Client ID in a call to *another* javascript file (myUserControl_startUp(thisClientId)). I could just as easily assign the ClientId to a variable and use it on the page, but I like to instead keep my Javascript external to the page as much as possible and instantiate it this way. It lets me more easily reuse functions across pages, and just feels like a cleaner separation of concerns.

3. Reference your elements

And finally, in that external JS, you can now get at your elements with jQuery and via code like the following:

function myUserControl_startUp(thisClientId) {
  //alert('thisClientId: ' + thisClientId);
  var selectedTabIdx = $('#' + thisClientId + '_ctl00_hiddenTabIdx').val();
}

Read Full Post »

Was making good progress with provisioning a site (with lists, content types, lookups, etc) in Sharepoint 2010, and thought I had the structural stuff licked. Moved on to the forms (dispform etc) and quickly found that I needed to include Dependant Lookups along with my Lookup Fields. This proved to be a bit tricky for me.

Dependant Lookups are new in Sharepoint 2010 and allow you to return virtual columns along with your Lookup Columns. They reveal themselves in the UI as checkboxes in the settings for Lookup Fields, and create additional columns in the format TargetList:SubColumn. So lets say I’ve got 2 lists: Customers and Orders that represent a one:many relationship. In Orders, I’ll need a Lookup Field (likely called “Customer”) that is a lookup to the Customers list.

On top of this, to make a parent-child form display correctly in the dispForm for Customers (ie a dispForm for a customer that shows all of their orders), I need to also reveal the ID of the Customer in the lookup field in Orders. So when I look at my content type for the Orders list, there will be an entry “Customer” of type lookup, as well as as entry “Customer:ID” of type lookup.

So I wrote the following method to create the lookup, add any additional dependant lookups desired, and then return the lookup field to the calling method (so it can then add it to the content type, etc).

        /// <summary>
        /// Create a site column, at the specified web level, of type lookup and wire it up to the appropriate list and column.
        /// This creates the field, which will then be ready to be added to a content type and applied to a list.
        /// Note this must be done AFTER referenced list instances have been initiated.
        /// </summary>
        public static SPFieldLookup CreateLookupField(string fieldDisplayName, string fieldStaticName, string group, bool required, bool allowMultipleValues,
            string siteName, string webName, string lookupListName, string lookupField, List<string> DependentLookupFieldNames)
        {
            using (SPSite spSite = new SPSite(siteName))
            {
                using (SPWeb spWeb = spSite.OpenWeb(webName))
                {
                    SPList lookupList = spWeb.Lists[lookupListName];

                    spWeb.Fields.AddLookup(fieldStaticName, lookupList.ID, lookupList.ParentWeb.ID, required);
                    SPFieldLookup lookup = spWeb.Fields[fieldStaticName] as SPFieldLookup;
                    lookup.Title = fieldDisplayName; // "Title" is Display Name
                    lookup.AllowMultipleValues = allowMultipleValues;
                    lookup.LookupField = lookupField;
                    lookup.Group = group;
                    lookup.Indexed = true;
                    //lookup.RelationshipDeleteBehavior = SPRelationshipDeleteBehavior.Restrict;//"A site column cannot enforce a relationship behaviour"
                    lookup.StaticName = fieldStaticName;
                    lookup.Update(true);

                    if (DependentLookupFieldNames != null)
                    {
                        // Create the secondary columns. ie what's editable via the web ui reading:
                        // "Add a column to show each of these additional fields:" Displays like "Committee:ID"
                        // ie depField will usually be "ID"
                        foreach (string depField in DependentLookupFieldNames)
                        {
                            string dependantFieldName = spWeb.Fields.AddDependentLookup(string.Format("{0}:{1}", fieldDisplayName, depField), lookup.Id);// eg "Committee:ID"
                            SPFieldLookup dependentField = spWeb.Fields.GetFieldByInternalName(dependantFieldName) as SPFieldLookup;
                            dependentField.LookupField = depField;
                            dependentField.Group = group;
                            dependentField.Update(true);
                        }

                        spWeb.Update();
                    }

                    return lookup;
                }
            }
        }

Read Full Post »

Build a list in Sharepoint 2010 and return the list as an object to continue working with

const string _CounterListName = "My List";
const string _Site = "http://SharepointDev/";
const string _Web = "WebName";

private SPList CreateList(string listName)
  {
    SPSite spSite = new SPSite(_Site);
    SPWeb spWeb = spSite.OpenWeb(_Web);

    if (spWeb.Lists.TryGetList(listName) == null)
      {
        spWeb.AllowUnsafeUpdates = true;
        spWeb.Lists.Add(listName, "This list was created programmatically", spWeb.ListTemplates["Calendar"]);
      }

    return spWeb.Lists[listName];
  }

UPDATE: I just keep learning new things with Sharepoint all the time… This is not the best way to createa new list, because SPSite and SPWeb are both IDisposable, and unless you explicitly dispose them, or wrap in usings, you can face memory leak issues.

Read Full Post »

Just spent an unreasonable amount of time getting remote debugging working with Sharepoint 2010 Sandbox *AND* Full-Trust solutions. It took quite a bit of work for me to understand and troubleshoot, so thought I would write it down:

a. for my own future reference

b. so others don’t have to go through the same heartache

The general approach for sandbox and full trust solutions is the same, but they do differ in a few details.

A post by tlhost on the Microsoft Forums titled A SharePoint server is not installed on this computer got me 90% of the way there, this is just filling in the gaps that tripped me up.

And a BIG THANK YOU to Pav – he helped a ton

Export Nodes from Sharepoint Machine Registry

Remote to the Sharepoint machine, run regedit and export [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\14.0]

On your development machine, run regedit and import the nodes.

Create Sharepoint project

You will now be able to create a Sharepoint Project in Visual Studio 2010. When asked where the Sharepoint server is, tell Visual Studio it’s on your local machine. You will not be able to deploy locally (unless you actually *do* have Sharepoint installed), but you will now be able to create Sharepoint projects, and will be able to package them.

Create a project. To test, I simply created a Blank Sharepoint Project (first one was sandboxed, second one was full trust), and added a webpart with a label displayed.

protected override void CreateChildControls()
{
base.CreateChildControls();
Label l = new Label() { Text = "BLAH!" };
this.Controls.Add(l);
}

Build the solution (so that you generate .pdb files) and then right click on the project in the Solution Explorer and choose “Package”. This will generate a .wsp file, which is actually a cab file (try changing the extension and opening in winzip or similar). If you extract it, you’ll see that in the .wsp is the webpart and the metadata files (xml).

Install .wsp to Sharepoint

This approach may differ between sandboxed and full trust:

Sandboxed

Fire up your web browser and navigate to the Sharepoint UI.

Go to Site Actions>Site Settings and click on “Solutions” under “Galleries”

Click “upload Solution”, navigate to your .wsp, upload it, and activate it.

You should now be able to add your webpart to a page

Full Trust

Copy your .wsp to the remote machine (anywhere, really)

Fire up Powershell_ise (an install of Powershell that Sharepoint installs and which includes a bunch of Sharepoint-specific cmdlets)

Run the command:

add-spsolution -LiteralPath "c:\SharePointProject4.wsp"

and then:

install-spsolution -Identity "SharePointProject4.wsp" -GACDeployment -WebApplication "http://RemoteSPInstallName"

But of course replace with the name of your .wsp and the web application that you want to install to (try running “get-spsite | get-spweb”, if you need to see the sites installed locally)

Your solution should now be installed into Sharepoint, you may have to use the UI and go to Site Actions>Site Settings>Site Collection Administration>Site Collection Features and activate your feature from the list before associated webparts become available.

Add your webpart to a page.

Get Remote Debugging Bits Set up

Pull the remote debugger folder from your VS2010 install at: C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\Remote Debugger over to the remote server. Note there is a 64 bit and a 32 bit version. Choose the correct one.

Make sure that the account that you use on your local machine also exists on the remote machine. They do not need to be in the same domain, but they must have the same name. Add the account to the appropriate group on the remote machine (administrators?).

Start the remote debugger “service” by double clicking on msvsmon.

Back on your local machine in VS2010, go to debug>attach to process and enter the domain, account and server that you are connecting to. NOTE that the account name is not necessarily your user account (ie the account you ensured exists on the remote machine above). It must be the account the THE REMOTE DEBUGGER IS RUNNING UNDER.

That is, if your account on your local machine is DOMAIN\jsmith, you have to make sure that a jsmith account exists on the remote machine as well, and that it has permissions to debug. But you might be logging on to the server (let’s call it “SHAREPOINTSERVER1”) with some service account like sharepointadmin_service and running the remote debugger bits as that account. In this case you would attach to the remote process with the “qualifier” of:

DOMAIN\sharepointadmin_service@SHAREPOINTSERVER1

On the remote machine, your debugger should show you as connected:

And in VS2010, you should now see the processes running on the remote server. Make sure you select to “Show processes from all users”.

Attach to Process and Deploy .pdbs

Before our breakpoints can be hit, we have to make sure that the .pdbs are accessible

Sandboxed

Sandbox solutions run in a different application pool than the rest of sharepoint (hence the “sandboxed” part; your webpart could go nuts and it won’t take down Sharepoint proper). Sharepoint will run in a W3WP process, but the sandboxed solutions will run in a process called the SPUCWorkerProcess.

Find and attach to the SPUCWorkerProcess.

Type CTRL+ALT+U in VS2010 to open the Modules panel

Look through the modules, and you should find your Sharepoint solution and see that VS cannot open its PDB (and breakpoints will not be hit)

Make a note of the path, in this case:

C:\ProgramData\Microsoft\SharePoint\UCCache\8CDA3229CD9F12A\1CAC709F235C40DF8E36CCE5B798E115%2DjtwtnquBLn%2B%2547Udab9QIvqZg0JeyEczf%2547ky%2BIAVMPDzs%3D-13\SharePointProject2.dll

By default, Sharepoint will store the deployed .dll in the c:\ProgramData\Microsoft\SharePoint\UCCache folder on the server. Note that “ProgramData” is a hidden folder, so you’ll have to enable viewing hidden folders and files.

I don’t quite understand the logic (ie when precisely it’s triggered), but the folder will change during the lifecycle of the solution. I believe that when the SPUCWorkerProcess gets recycled, Sharepoint will create a new subfolder, but I’m not sure under what circumstances this occurs. Point being, the “active” .dll is in this folder now, but next time you try to debug, it might be in a different one.

Copy your .pdb file to the specified directory on the remote server

Now you should be able to right-click on the Module in the Modules Pane and select Load Symbols From>Symbol Path

Once you’ve done this, your breakpoint should become “hittable” and the symbols shown as “Loaded”

Reload the web page, and boo-yah, you’re debugging remotely!

Full Trust

In some ways, debugging full trust is a little easier, and in some ways a bit harder 🙂 (so I guess that means it’s about the same?) Full trust solutions run in the main Sharepoint W3WP process; however, Sharepoint spins up multiple W3 worker processes, so you have to connect to the proper one.

On the remote server, back in Powershell_ise execute:

c:\Windows\System32\inetsrv\appcmd list wp

This will return a list of all W3 worker processes, with a bit of descriptive info and process id. You want to make a note of the process ID of the “main” Sharepoint process (generally running on port 80)

Back in Visual Studio, debug>attach to process and find and attach to the W3WP Process on the remote server that matches the process identified above.

Open the modules pane by typing CTRL+ALT+U

Look through the modules, and you should find your Sharepoint solution and see that VS cannot open its PDB (and breakpoints will not be hit)

Make a note of the path, in this case:

C:\Windows\assembly\GAC_MSIL\SharePointProject4\1.0.0.0__bf506a3a24d83b2c\SharePointProject4.dll

Sharepoint stores deployed Full Trust .dlls in the GAC.

A little trick here: in order to navigate the GAC as a normal folder structure, MAP A NETWORK DRIVE on your development system to \\SERVER\C$\Windows\assembly. Go through the mapped drive, and find the install location mentioned above (eg “GAC_MSIL\SharePointProject4\1.0.0.0__bf506a3a24d83b2c\SharePointProject4.dll”). Copy your .pdb file here

Now right click on the module in the module pane and select Load Symbols From>Symbol Path

Once you’ve done this, your breakpoint should become “hittable” and the symbols shown as “Loaded”

Reload the web page, and boo-yah X 2, you’re debugging remotely!

A few lingering issues

Does the .pdb *REALLY* have to go in the GAC?

I’ve read elsewhere (eg: here) that you don’t have to put the .pdb in the GAC. I haven’t been able to get it to work, however. If anyone has more info on this, I’m all ears.

How about updates to my solutions?

For Full Trust solutions, I believe that simply replacing the .dll and .pdb in the GAC will get you there… Haven’t done enough yet to know otherwise. You could be more thorough and:

Uninstall-SPSolution -Identity "SharePointProject4.wsp" -WebApplication "http://RemoteSPInstallName/"
Remove-SPSolution -Identity "SharePointProject4.wsp"

in powershell_ise

For Sandbox Solutions, I go through the UI and remove and add through the Solution Gallery

Tell me more

If you have any question, corrections, or comments, be sure to post them below

cheers

Read Full Post »