Thursday 30 October 2008

Uploading Files to MOSS 2007 via the SharePoint Object Model

I've blogged on a similar topic about a year ago, but the previous post was using the Copy.asmx web service directly (see http://ddkonline.blogspot.com/2008/01/uploading-files-to-moss-2007-via.html). Here is the equivalent using the SharePoint object model when you have the luxury of being hosted on the WSS/SharePoint box. One thing to note is that you will chew up memory if you don't dispose your SPWeb and SPSite objects correctly (either via using or by doing explicit Dispose() calls) - you cannot rely on .NET Framework garbage collection to clean them up for you. See inline comments below on this.

Background: The current Management App I'm working on has an IIS application hosted in Atlanta as the frontend to a Windows SharePoint Services (WSS) document store hosted in Australia. The problem with this was that the upload would do a massive round-trip overseas and back again - so a large file (say 20MB would take well over an hour). The workaround to this was to host the page as an IFrame in our application and to host it on a WSS Server so uploads of large files could be done directly to WSS rather than via the IIS Server overseas. I used the following code in the hosted page to perform the uploads using the SharePoint 2007 object model.

/// <summary>
/// Upload documents using MOSS Object Model Directly, updates database with document information
/// After the upload of each document is complete.
/// </summary>
/// <param name="projectId"></param>
/// <param name="sourceStream"></param>
/// <param name="fileName"></param>
/// <returns></returns>
public static VoidResponse UploadDocument(UploadedFile uploadedFile,
string projectNumber, int projectId, int activityId, int documentTypeLookupId,
int constructionAuthorisationId, string subfolderPath)
{
//Core document information to be passed into MOSS
//and into the .NET Web Service to update the database
DocumentDto newDocument = new DocumentDto();
newDocument.ProjectNumber = projectNumber;
newDocument.Name = uploadedFile.GetName();
newDocument.SharepointListName = ConstructSharepointListName(newDocument.ProjectNumber);
//Snapshot path as combination of both the
newDocument.SharepointSnapshotPath = CurrentSnapshotPath;

if (string.IsNullOrEmpty(newDocument.SharepointFileRelativePath))
{
newDocument.SharepointFileRelativePath = newDocument.Name;
}

string queryOptions = FormatHelper.GetQueryOptions(projectNumber, CurrentSnapshotPath, subfolderPath);

//To Allow return of uploaded file details
VoidResponse response = new VoidResponse();

//Housing SPSite and SPWeb in using blocks so they are disposed correctly as per best practices
//in http://msdn.microsoft.com/en-us/library/aa973248.aspx
using (SPSite site = new SPSite(SharePointParameter.SharePointSiteUrl))
{
using (SPWeb web = site.OpenWeb()) //The SPSite already has the site url value.
{
try
{
web.AllowUnsafeUpdates = true; //Using AllowUnsafeUpdates because we are creating an SPWeb -
//otherwise will result in exception
//as per http://hristopavlov.wordpress.com/2008/05/16/what-you-need-to-know-about-allowunsafeupdates/
SPList list = web.Lists[newDocument.SharepointListName];

SPFileCollection fileCollection = null;

//Using query options as recommended by
//http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.splist.aspx,
//same as the Web Services do in the LL framework.
SPQuery spQuery = new SPQuery();
spQuery.Query = queryOptions;
SPListItemCollection folderCollection = list.GetItems(spQuery);

//Folders are really just SPListItems with type of FilSPFileSystemObjectType.Folder
//Defensive: Folder not found exception
if (folderCollection.Count == 0) //Error condition - folder not found
throw new ConfigurationErrorsException(string.Format(Messages.Pcp_MOSS_FolderNotFoundError,
newDocument.SharepointSnapshotPath));

fileCollection = folderCollection[0].Folder.Files;

//Only perform duplicate check when it is turned on in configuration settings.
if (SharePointParameter.CheckForDuplicateFilesInMOSS)
{
//Check if file doesn't already exist.
foreach (SPFile file in fileCollection)
{
if (file.Name == newDocument.Name)
{
//Duplicate detected
response.IsSuccessful = false;
response.Messages.Add(new Message(string.Format(Messages.Pcp_DuplicateFileDetected,
file.Name)));
return response;
}
}
}

Stream fileStream = uploadedFile.InputStream;
byte[] contents = new byte[fileStream.Length];
fileStream.Read(contents, 0, (int)fileStream.Length);
fileStream.Close();

//Upload document itself. This will also give us the new Id to store in the database.
SPFile currentFile = null;

currentFile = fileCollection.Add(newDocument.Name, contents);
System.Security.Principal.WindowsImpersonationContext impersonationContext = null;

try
{
if (GetUseCurrentUserCredentialsForMOSSFlag()) //Impersonate if uploading document
{
impersonationContext =
((System.Security.Principal.WindowsIdentity)
HttpContext.Current.User.Identity).Impersonate();
}

//Upload file details to main PCPData database
PcpDataService.PcpDataService pcpDataService = new PcpDataService.PcpDataService();
pcpDataService.Url = DDKOnline.WssHostedWeb.Shared.SystemParameter.DDKOnlineDataServiceUrl;
pcpDataService.AddProjectDocument(projectId, activityId, constructionAuthorisationId,
currentFile.Item.ID.ToString(), newDocument.SharepointListName,
newDocument.SharepointSnapshotPath, newDocument.SharepointFileRelativePath,
documentTypeLookupId, newDocument.Name);
}
finally
{
if (GetUseCurrentUserCredentialsForMOSSFlag() && impersonationContext != null)
{
impersonationContext.Undo();
}
}
//Successful upload completed.
response.IsSuccessful = true;
response.Messages.Add(new Message(Messages.Pcp_SharePointUploadComplete));
}
catch (SPException spException)
{
//Error ocurred during upload
response.Errors.Add(new DDKOnline.Framework.Common.Response.Error(spException.Message));

response.IsSuccessful = false;
}
catch (Exception ex)
{
response.Errors.Add(
new Error(string.Format(Messages.Pcp_SharePointAccessFailed)));
ExceptionPolicy.HandleException(ex, SERVICE_EXCEPTION_POLICY);
response.IsSuccessful = false;
}
finally
{
if (web != null) web.AllowUnsafeUpdates = false;
}
}
}
return response;
}


/// <summary>
/// Generate query options to support storage within subfolders of current list
/// </summary>
public class FormatHelper
{
internal static string GetQueryOptions(string projectNumber, string CurrentSnapshotPath, string subFolderPath)
{
string folderPath = VirtualPathUtility.RemoveTrailingSlash(string.Format("{0}/{1}/{2}", projectNumber, CurrentSnapshotPath, subFolderPath));
string queryOptions = string.Format("<Query/><QueryOptions><Folder>{0}</Folder></QueryOptions>", folderPath);
return queryOptions;
}
}

Tuesday 28 October 2008

How do I get the underlying type of a Generic List?

The Type.GetGenericArguments() method will let you determine the underlying type of the elements of a list object at runtime. See this article for more information.

http://msdn.microsoft.com/en-us/library/system.type.getgenericarguments.aspx

For example:


if (t.IsGenericType)
{
// If this is a generic type, display the type arguments.
//
Type[] typeArguments = t.GetGenericArguments();
Console.WriteLine("\tList type arguments ({0}):", typeArguments.Length);
    foreach (Type tParam in typeArguments)
{
// If this is a type parameter, display its
// position.
//

        if (tParam.IsGenericParameter)
{
Console.WriteLine("\t\t{0}\t(unassigned - parameter position {1})",tParam,
tParam.GenericParameterPosition);
}
else
{
Console.WriteLine("\t\t{0}", tParam);
}
}
}

Wednesday 15 October 2008

Zach William Klein arrives!

Our fabulous new baby boy Zach William Klein ("Zacky Zack") was born 10/10/2008 10:19am with a championship weigh-in of 3.855kg. We have just brought him home from hospital and after a shocker of a night, he has calmed down to 3 sleeps a night on day 2. Welcome home Zach!

For many more first piccies see http://www.ddkonline.com/Gallery/2008_10_October_ZachKlein_Day1/gallery.html or my most recent FaceBook photo gallery.










Tuesday 7 October 2008

How to do remote debugging with the Visual Studio 2008 Remote Debugger Service (Msvsmon.exe)

There is always a problem that will crop up on one of your servers that you just CANNOT reproduce at all. To solve pesky problems like this, you can make use of the remote debugger service of Visual Studio. What's more, you can debug without running a setup package at all on dev or production servers. Instead, you can just run msvsmon.exe from a file share without even installing anything. This will definitely keep the network guys happy!

Typically, you can get msvsmon.exe file from the following path on your development machines that already have Visual Studio 2008 installed:
C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\Remote Debugger\x86\msvsmon.exe

When you run Msvsmon.exe, it just shows up on the server as a windows application on your server (See screenshot). You can also run it as a Windows Service so you don't need to log onto the server to start it up.




In Visual Studio on your local machine, you then just put in a fully qualified remote server name in "Attach to Process" dialog. Now you can debug the remote machine to your hearts content (make sure you're a debugger user or remote admin otherwise you will get access denied errors).

Just a reminder - if you are debugging ASP.NET on a 2003 server and above, there will be 1 w3wp.exe process for each of your application pools. It may be hard to find out which w3wp.exe process you want to debug as there may be many application pools. You can find out which w3wp.exe process is running your pool by a process of deduction (ie just by stopping the application pools you are not using. The one left is yours).

For the full goss, see http://support.microsoft.com/default.aspx/kb/910448. There is even a 17 minute tutorial on setting it up. See http://www.microsoft.com/uk/msdn/screencasts/screencast/313/visual-studio-2008-remote-debugging-with-msvsmonexe.aspx

Just keep in mind - you may be up for a long wait if the connection to your server is slow - especially if attaching a process in Atlanta! :o). There is a timeout option if you get timeout errors (you only get timeout errors in "No Authentication" mode as the "Windows Authentication" mode has an infinite timeout.)


System.Web.VirtualPathUtility to get File Names from Urls and Converting Relative Paths to Absolute Urls (without ResolveUrl)

One of the things that appeared to be lacking in ASP.NET 2.0 and above (or so I thought) is a utility library to parse and extract paths and get the file name from a Url. Here are some examples of the url parsing code that I and others have done without knowledge of this library: http://www.thejackol.com/2007/04/10/get-file-name-from-url-cnet/
and http://www.west-wind.com/Weblog/posts/154812.aspx

One class that does just this (and which most developers don't know about) is System.Web.VirtualPathUtility(). Everyone knows it's brother utility library System.Web.HttpUtility but VirtualPathUtility should be an essential part of any ASP.NET developer's toolkit. See http://msdn.microsoft.com/en-us/library/system.web.virtualpathutility.aspx for more information.