Wednesday 30 January 2008

Uploading Files to MOSS 2007 via the Copy.asmx Web Service

Unlike the imaging web service (SharepointSite/_vti_bin/imaging.asmx), there is no "Upload" web method available for normal lists. There is a way around this lack of functionality in the standard MOSS lists.asmx web service.

You can upload files into your Sharepoint repository in one hit along with metadata (ie custom column information and values). You just have to Reference the SharepointSite/_vti_bin/copy.asmx (e.g. http://dev-moss/sites/home/PropertySharePoint/_vti_bin/copy.asmx) to get access to the Copy.CopyIntoItems() method. You then simply pass in the stream and Array of FileInfo objects (which have your metadata) into this method. I have seen several examples around that use the Http PUT method (e.g. http://www.sharepointblogs.com/ssa/archive/2006/11/30/wsuploadservice-web-service-for-uploading-documents-into-sharepoint.aspx)and and then grab the file back and update it with the meta data. My code sample below shows a much simpler way. I have not seen this technique is some of the larger Development guides such as SAMs MOSS 2007 Development Unleashed - they only give a passing mention to the copy service. MSDN also brushes over this service - http://msdn2.microsoft.com/en-us/copy.copy.copyintoitems.aspx



/// <summary>
///
/// </summary>
/// <param name="listName"></param>
/// <param name="destinationfolderPath"></param>
/// <param name="sourceFileSteam"></param>
/// <param name="fileName"></param>
/// <param name="fields"></param>
/// <returns>String with the destination Uri</returns>
public uint UploadFile(string listName, string destinationfolderPath, Stream sourceFileSteam, string fileName, SharepointCopyProxy.FieldInformation[] fields )
{
//Create folder if it doesn't exist
CreateFolder(listName, destinationfolderPath);

byte[] fileBytes = new byte[sourceFileSteam.Length];
sourceFileSteam.Read(fileBytes, 0, (int)sourceFileSteam.Length);
string[] destinationUri = {string.Format("{0}/{1}/{2}/{3}",_urlSiteRoot, listName, destinationfolderPath, fileName)}; //This may have issues as the listname may be different to the full path - may need to get value from folder path
SharepointCopyProxy.CopyResult[] result;

uint documentId = _copyWebService.CopyIntoItems("http://null", destinationUri, fields, fileBytes, out result);

if (result[0].ErrorMessage != null)
{
throw new System.ApplicationException("An error occurred uploading the file to Sharepoint.", new System.Exception(result[0].ErrorMessage));
}
return documentId;
}

Thursday 17 January 2008

The simplest way to upload documents and files to MOSS with the ASP.NET System.Net.WebClient class

It is odd that the Sharepoint 2007 Imaging Web Service has an Upload() Web Method, but the List Web service (lists.asmx) does not. To overcome this limitation, there is a very simple way to upload a file to your Sharepoint server using System.Net.WebClient (I have seen some methods using 30+ lines of code and HttpRequest, so it's best to use this method!):


//e.g. destinationFullURI = "http://dev-moss/sites/home/PropertySharePoint/DocumentLibrary/0/tmp167.tmp"
//sourceFullPath = "c:\\temp\myfiletoupload.tmp"

WebClient webClient = new WebClient();
webClient.Credentials = _credential;
webClient.UploadFile(destinationFullURI, "PUT", sourceFullPath);


Make sure you include the "PUT" method so it doesn't attempt an HTTP post (which is the default behaviour).

Tuesday 15 January 2008

MOSS - Getting items in a particular subfolder of a Picture Library via Sharepoint Web Service

[UPDATE 2007/01/16] - While this is a bug that Picture Libraries don't work properly within the normal Document Library web services, I believe that the Imaging Web service should be used for picture libraries rather the normal lists.asmx i.e.
http://[server]/[site]/_vti_bin/imaging.asmx.

An excellent chapter on this web service can be found in Sams Microsoft SharePoint 2007 Development Unleashed
[/UPDATE 2007/01/16]

It appears that you cannot filter a Picture Library by Folders using the QueryOptions parameter for .GetListItems(). I consider this to be a bug in the MOSS Web Service querying engine.

You can download a very handy tool from here to demonstrate the issue easily http://www.u2u.info/Blogs/karine/Lists/Posts/Post.aspx?List=d35935e0%2D8c0e%2D4176%2Da7e8%2D2ee90b3c8e5a&ID=12


To reproduce the bug:

  1. Make a photo library with 2 subfolders
  2. Make a normal document library with 2 subfolders
  3. Adding several photos to this photo library
  4. Adding several photos to the document library
  5. Opening up the tool, right-clicking on your list and selecting "GetItems" from the context menu
  6. Go to the View Options tab and entering your document path (e.g. MyListName/MySubfolderName)
  7. You will get the whole list from the picture library regardless of this parameter..... BUG!
  8. Try the same thing on the document library - and the Folder option works as expected.



FIGURE: The filter not working properly on Photo Library (returns whole list including folders) when a subfolder is specified in the filter.




FIGURE: The filter working correctly on Document Library as expected (returns subset of data)


In particular, this CAML will not work on a Photo Library where my List Name is 'PhotoLibrary' and my subfolder is '1':




<Query />
<ViewFields />
<QueryOptions>
<Folder>PhotoLibrary/1</Folder>
</QueryOptions>

Monday 14 January 2008

MOSS - Getting a all items within a Sharepoint List recursively

When you query a list in MOSS, you only get the top level folders by default. If you have a folder hierarchy within your Sharepoint 2007 list, you will not see the documents underneath the top level of your list. It is important to note that you shouldn't go through each folder in this top level and then querying each folder for a list again. Instead, you should just use the Scope="Recursive" setting
(Add to your queryoptions Xml fragment).

For more info, see the following:

http://sqlblogcasts.com/blogs/drjohn/archive/2007/11/02/Getting-a-list-of-files-from-a-moss-document-library-using-a-SharePoint-web-service.aspx

There is also an element you can add to the the Xml fragment to to restrict the results to one folder - the "Folders" element. For more details, see:

http://msdn2.microsoft.com/en-us/library/lists.lists.getlistitems.aspx

For example, to recursively get all documents in folder a folder named "Folder 10", your Xml fragment would be

"<queryoptions><viewattributes scope="\"><folder>Folder 10</folder></queryoptions>"

Another article I found useful on recursively enumerating folder contents is at:
http://blog.krichie.com/2007/01/30/traversing-sharepoint-list-folder-hierarchies/

Thursday 10 January 2008

Subversion Branching And Merging (using TortoiseSVN)

A sad fact of life is that you sometimes have to do things you don't agree with. For my last 2 clients (one for SSW - Pfizer and one for Oakton - Lend Lease), I have had to use Subversion (SVN) rather than Microsoft's championed Team Foundation System (TFS). One of the features lacking in subversion is what TFS calls shelving.

You've got to use what you've got (SVN) to your best advantage but Andre Gallo, one of my colleagues from Lend Lease, has been struggling with it. In particular he's been having some serious issues with getting the current HEAD version from the trunk into his revision - and from the revision to the main trunk of our source code repository. Here are some of the discoveries I made this afternoon when trying to get branching and merging working smoothly for Andre, myself and the rest of our team:

We received some documentation from one of my colleagues from Oakton (Alexey Greyze) to assist us getting up and running with a good branching solution using subversion. One thing from his documentation that I did not agree with is to have a branch per JIRA ticket (aka one branch per development task). I regard this as excessive and have decided on a much simpler file system/structure for our branching. One thing that is also not clear from the SVN documentation is the fact that the merge tool is used to replay changes on your current working copy - NOT to merge from one branch to the other (e.g. from the branch to the trunk) directly.

As is suggested by many articles (e.g. http://devnulled.com/content/2006/10/guide-and-best-practices-for-subversion-branching/), excessive merging between branches is complicated to work with and should be avoided. You should try and have as many developers working on the trunk (or a single branch e.g. the dev branch) as possible. This helps to avoid massive merging sessions after 2 weeks of changes that can cause problems. As one article put it "Branches are for the minority." . The only reason to use branches per developer is if you have a team where communication is difficult (which doesn't apply in our case) and it is hard to get them to fix up issues if they check bad code into the trunk/central branch.

We will keep it simple and just have the following structure:
  1. /Dev folder
  2. /Trunk folder
  3. /Build folder (used by Cruise Control for daily builds)

One of the articles I came accross also helped us to understand how merging works with Branching can be found here:
http://nedbatchelder.com/text/quicksvnbranch.html

A CRITICAL point of understanding about the Tortoise SVN Merge dialog (and the SNV merge process in general) is that the "FROM" and "TO" sections of the dialog are not that you are merging between 2 different locations. Instead, the "FROM" and "TO" sections indicate that you are REPLAYING each change in a single location (e.g. http://subversion.apac.lendlease.com/svn/IT/Global/LendLease.IM/dev/davidklein/LPP-117) from revision 100 to revision HEAD on the folder that you right clicked on for the merge using SVN. If I right-clicked on the trunk folder and clicked merge, this would replay all changes logged on http://subversion.apac.lendlease.com/svn/IT/Global/LendLease.IM/dev/davidklein/LPP-117 betwen 100 and HEAD on my working copy of the trunk.

Note that this dialog is basically saying it is going to replay all the changes made between 100 and my head revision on my current working folder (ie the one I right clicked on and clicked "Merge")

When you are merging back to the trunk, you may also run into the following error:

Error: Unable to find repository location for 'http://subversion.apac.lendlease.com/svn/IT/Global/LendLease.MasterDataManager/trunk' in revision 32797

To fix this, you just need to change the revision number to the one after you did the branch. ie 32798 in this example.

Wednesday 9 January 2008

PRB: You get the error "Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control"

I ran into this issue again today in an ASP.NET 3.5 project (when controls auto postback inside a formview/detailsview). After the postback, you get the error "Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control".

According to this article:
http://www.webswapp.com/codesamples/aspnet20/dependentlists/default.aspx

"When you have a dropdownlist that is dependent on another within a FormView server control, it becomes populated on SelectedIndexChanged event of the first dropdown list. However, in this case the template was not rebound to the data, only the dropdown list. The FormView was reconstructed from the saved ViewState. This causes the error message: "Databinding methods such as Eval(), XPath(), and Bind() can only be used in the context of a databound control".
The solution is to remove the 2-way databinding from the cascading dropdownlists and replace it with customized code during processing the ItemUpdating event. "

For more details about the solution, see http://www.webswapp.com/codesamples/aspnet20/dependentlists/default.aspx

Tuesday 8 January 2008

Integrating ASP.NET applications with Sharepoint

This article provides a good rundown on the various ways that you can get ASP.NET applications and MOSS 2007 talking to each other.
http://blogs.msdn.com/sharepoint/archive/2007/10/26/integrating-sharepoint-with-other-portals-and-applications.aspx

One of my requirements for the property pipeline application here at Lend Lease is to provide a photo gallery that reads and writes to Sharepoint from an ASP.NET 3.5 app. Of course, the simplest way to do this would be to host the list in an iframe if possible...

Thursday 3 January 2008

You receive an error "No keys specified for the current object data type" when using an ObjectContainerDatasource in ASP.NET 3.5

Just in case anyone encounters the same issue and can't work it out - I had this error crop up when doing 2-way databinding with an object container datasource (OCDS).

"No keys specified for the current object data type"

When I made my programmatic call to FormView.UpdateItem(false), I was constantly getting this error.

Turns out I was rebinding the FormView outside a postback handler. The fix was to move the
FormView.DataBind() to inside a Page.IsPostback check.

There can also be an issue with bound controls "losing" the DataKeyNames property. You can fix this issue by specifying the DataKeyNames in code - e.g. addToAssetFormView.DataKeyNames = new string[] {"ObjectId"}; before the control is bound (or rebinding with addToAssetFormView.DataBind();)

This problem can occur with any databound controls like gridviews, not just formviews.

Wednesday 2 January 2008

ASP.NET 2.0 + Profiles

I had some questions today about the Profile Provider and why it was not working in a particular setup. It was a problem with their settings in the web.config. Here is one of the clearer explanations of how to set up the config.
http://www.theserverside.net/tt/articles/showarticle.tss?id=CreatingProfileProvider

This MSDN article is also handy: http://quickstarts.asp.net/QuickStartv20/aspnet/doc/profile/default.aspx

All you have to do is add settings like below to the web.config:

<?xml version="1.0"?>
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0">
<system.web>
<profile>
<properties>
<add name="BirthDate" type="DateTime"/>
<add name="FavoriteNumber" type="int"/>
<add name="Comment" type="string"/>
<add name="FavoriteColor" type="string" defaultValue="Blue"/>
<add name="FavoriteAlbums"
type="System.Collections.Specialized.StringCollection"
serializeAs="Xml"/>
</properties>
</profile>
</system.web>
</configuration>


Then use HttpContext.Current.Profile like this (no additional classes required):



// BirthDate is DateTime
textDob.Text = Profile.BirthDate.ToShortDateString();

// FavoriteNumber is Int32
textFavNumber.Text = Profile.FavoriteNumber.ToString();

// Comment is String
textComment.Text = Profile.Comment;

// FavoriteColor is String
dropDownFavColor.SelectedValue = Profile.FavoriteColor;

// FavoriteAlbums is StringCollection
foreach (string album in Profile.FavoriteAlbums)
{
listBoxFavAlbums.Items.FindByText(album).Selected = true;
}



The real question is: where does the strongly typed Profile come from? The ASP.NET compiler parses web.config to uncover the profile schema defined inside, and then code-generates a class in the Temporary ASP.NET Files directory. This is the class that is created when you add these elements to the web.config:



public class ProfileCommon : System.Web.Profile.ProfileBase {

public virtual short Age
{
get
{
return ((short)(this.GetPropertyValue("Age")));
}
set
{
this.SetPropertyValue("Age", value);
}
}

// other properties ...
}



See http://www.odetocode.com/Articles/440.aspx for a sample of this code and a more detailed walkthrough of how the httpmodules and proxy classes created by .NET are created and used.