Events to Expect when Dynamically Loading iFrames in JavaScript
18 June 09 07:49 AM | jacobl | 0 Comments   

chrome-ie-firefox1 Almost every JavaScript project starts off with a conversation about browser compatibility. As much as we'd like to think that JavaScript is JavaScript, each browser has its own implementation; and depending on what you're trying to do, those implementations could be drastically different from one another.

This case of inconsistent JavaScript behavior can be found in the events you can expect to receive during and after the dynamic loading of iFrames.

After quite a bit of testing and head scratching, we were able to come up with the following table of events to expect when dynamically loading content into an iFrame:

2table

What is Content-Disposition?

Sometimes when you provide your users with a file download link from a web page, you might not be linking them directly to the file, but to an ASPX page or PHP script instead, which streams the content of the file back to the client. In order for your browser to know what to do with this content, you provide header information with the response including Content-Type and Content-Disposition.

Content-Type is exactly what it sounds like: "application/pdf" if the content is PDF, "image/jpeg" if the content is JPEG, and so on. Content-Disposition refers to how you want the browser to handle the file after it is downloaded. It comes in a few flavors, but these are the ones that matter for this article: "inline" where the browser will attempt to load it (or an application that can load it) in the browser window or frame specified, and "attachment" where the browser will prompt the user to download.

Table Breakdown

The table above is for quick reference. I'm sure I'm going to need to Google for this answer in the future, so, "You're welcome, future-self." Here's a breakdown of each browser and what events should be expected and when they fire.

Safari / Chrome

safari_logo_3023 Inline: The onload event fires when the content is fully downloaded (but its associated application might not be done loading the file).

Attachment: ... thanks for nothing. Neither of these browsers provide any events for iFrames loaded with content that have an "Attachment" disposition type.

Firefox

Inline: This behaves identically to Safari / Chrome in the Inline case.

20070520-firefox_logoAttachment: This also behaves identically to Safari / Chrome in the Inline case.

IE (surprisingly gets my vote of approval here) There is an onreadystatechange event that fires whenever the iFrame's readyState property changes. That readyState reflects where the download is in the process.

Inline: When you initially set the src value of the iFrame element, the readyState changes to loading. When the file has completely downloaded, the readyState changes to interactive. The big difference between IE and the other browsers is that IE then changes the readyState property to complete when the page (or application) is fully loaded and ready for the user.

2840957763_b3672846e5Attachment: This behaves identically to the Inline case of IE, but the readyState property never changes to complete. That wouldn't make much sense, since the user has to manually open the file by double-clicking on it or opening  it from some application.

Final Thoughts

As you can see, this can be quite a pain; but with a little bit of research and knowing exactly what you want your customers to experience, you can have a fairly consistent process for all browsers.

Filed under: , ,
Using YUI Compressor with eval
16 June 09 08:16 AM | jacobl | 0 Comments   

YUI Compressor is an excellent JavaScript minifier and obfuscator. But every good thing comes with its compromises. While this tool prides itself on getting some of the best minification numbers I've seen, it's also very reliable. Your code gets smaller but will almost definitely work the same. While a few tools out there find trouble with eval and with, YUI Compressor gets around it by skipping it completely. It refuses to touch any code that is within the scope of an eval or with call. That might produce highly reliable, minified code, but in some cases, the code will just be reliable... unminified code. Good but not great.

Basic Case

Below is the example we'll be working with. We'll use it to demonstrate how eval can get trapped inside a scope causing YUI Compressor to ignore it completely.

   1: var Test = new function() {
   2:     var _self = this;
   3:     
   4:     function performOperation(scr) {
   5:         return "Result: " + eval(scr);
   6:     }
   7:  
   8:     _self.Operate = function(x, y, operation) {
   9:         return performOperation(x + operation + y);
  10:     }
  11: }

Above, the eval can reference anything inside the test class. It's hard to see how YUI could screw this up, but if we added an internal variable and modified the performOperation method to make use of it, you could see that things might get sticky.

   1: var Test = new function() {
   2:     var _self = this;
   3:     var _internalVariable = 3;
   4:  
   5:     function performOperation(scr) {
   6:         return "Result: " + eval("(" + scr + ") * _internalVariable");
   7:     }
   8:  
   9:     _self.Operate = function(x, y, operation) {
  10:         return performOperation(x + operation + y);
  11:     }
  12: }

This makes it a little more obvious. If YUI Compressor had it's way with this and wasn't as cautious as it is, _internalVariable would be renamed to some arbitrary string of letters and the eval call would fail.

You can see this by running the compressor on our script. The following code is the result:

   1: var Test=new function(){var _self=this;var _internalVariable=3;
   2: function performOperation(src){return"Result: "+eval("("+src+") * _internalVariable")}
   3: _self.Operate=function(x,y,operation){return performOperation(x+operation+y)}};

Nothing changed (return lines added for readability).

By adding the requirement that eval can only be called on public facing methods, we are then able to pull it out of the Test object entirely.

   1: var Test = {
   2:     Eval: function(src) {
   3:         return eval(src);
   4:     }
   5: }
   6: Test.Operator = new function() {
   7:     var _self = this;
   8:  
   9:     function performOperation(src) {
  10:         return "Result: " + Test.Eval(src);
  11:     }
  12:  
  13:     _self.Operate = function(x, y, operation) {
  14:         return performOperation(x + operation + y);
  15:     }
  16: }

While this allows for much more minified JavaScript, it does limit us a bit much. We can remedy this by adding a scope variable to the Test.Eval signature, allowing us to pass in the appropriate scope.

   1: var Test = {
   2:     Eval: function(src, scope) {
   3:         return eval(src);
   4:     }
   5: }
   6: Test.Operator = new function() {
   7:     var _self = this;
   8:     _self.Variable = 3;
   9:     
  10:     function performOperation(src) {
  11:         return "Result: " + Test.Eval("(" + src + ") * scope.Variable", _self);
  12:     }
  13:  
  14:     _self.Operate = function(x, y, operation) {
  15:         return performOperation(x + operation + y);
  16:     }
  17: }
  18:  

It might look a little confusing, but read it over; it's right.

Since YUI Compressor will not touch any code that has eval in its scope, we don't have to worry that the scope property will change.

   1: var Test={Eval:function(src,scope){return eval(src)}};
   2: Test.Operator=new function(){var a=this;a.Variable=3;
   3: function b(c){return"Result: "+Test.Eval("("+c+") * scope.Variable",a)}
   4: a.Operate=function(c,e,d){return b(c+d+e)}};

The only bit of code that is not minified is the Test.Eval method due to its use of eval. (return lines added for readability)

While this might not show much benefit as code-length savings goes, keep in mind that the example above is very small. Any reasonably long class will show huge improvements when minified properly; not to mention the benefits that are had from making your intellectual property less readable.

Real World Example

I will detail the following code in a future post, but for now, I wanted to show how the above technique of extracting eval calls out of a class can benefit JavaScript length after compression.

   1: var IAT = {};
   2: IAT.Eval = function(scr, scope) {
   3:     return eval(scr);
   4: }
   5: IAT.Importer = new function() {
   6:     var _self = this;
   7:     var _waitingClasses = [];
   8:     
   9:     function processWaitingClasses() {
  10:         for(var i = 0; i < _waitingClasses.length; i++) {
  11:             var refs = _waitingClasses[i].refs;
  12:             
  13:             var removeRefs = [];
  14:             for(var k = 0; k < refs.length; k++) {
  15:                 if(isDefined(refs[k]))
  16:                     removeRefs.push(k);
  17:             }
  18:             
  19:             for(var k = removeRefs.length - 1; k >= 0; k--) {
  20:                 refs.splice(removeRefs[k], 1);
  21:             }
  22:                 
  23:             if(_waitingClasses[i].refs.length < 1) {
  24:                 var cls = _waitingClasses[i];
  25:                 _waitingClasses.splice(i, 1);
  26:                 setClass(cls);
  27:                 break;
  28:             }
  29:         }
  30:     }
  31:     
  32:     function setClass(def) {
  33:         // 'scope' will be evaluated as def in IAT.Eval
  34:         IAT.Eval(def.clsName + '=scope.cls();', def);
  35:         processWaitingClasses();
  36:     }
  37:     
  38:     function isDefined(str, scope) {
  39:         if(!scope) scope = 'window';
  40:         var path = str.split('.');
  41:         
  42:         var scr = scope + '.' + path[0];
  43:         if(!IAT.Eval(scr)) return false;
  44:         
  45:         if(path.length == 1) return true;
  46:         
  47:         path.splice(0,1);
  48:         
  49:         str = path.join('.');
  50:         if(path.length > 1) str = str.substring(0, str.length);
  51:         
  52:         return isDefined(str, scr);
  53:     }
  54:     
  55:     _self.Ns = function(str, scope) {
  56:         if(!scope) scope = 'window';
  57:         var path = str.split('.');
  58:         
  59:         var scr = scope + '.' + path[0];
  60:         if(!IAT.Eval(scr)) IAT.Eval(scr + '={};');
  61:         
  62:         if(path.length == 1) return;
  63:         
  64:         path.splice(0,1);
  65:         
  66:         str = path.join('.');
  67:         if(path.length > 1) str = str.substring(0, str.length);
  68:         
  69:         this.Ns(str, scr);
  70:     }
  71:     
  72:     _self.DefineClass = function(config) {
  73:         var parent = config.clsName.substring(0, config.clsName.lastIndexOf('.'));
  74:         _self.Ns(parent);
  75:         _waitingClasses.push(config);
  76:         for(var i = 0; i < config.refs.length; i++) {
  77:             _self.Import(config.refs[i]);
  78:         }
  79:         processWaitingClasses();
  80:     }
  81:     
  82:     _self.Import = function(clsName) {
  83:         if(isDefined(clsName)) return;
  84:     
  85:         var path = clsName.split('.');
  86:         path.splice(0,1);
  87:         var clsPath = path.join('/');
  88:         clsPath = clsPath.substring(0, clsPath.length);
  89:         
  90:         var head = document.getElementsByTagName('head')[0];
  91:         var script = document.createElement('script');
  92:         script.setAttribute('type','text/javascript');
  93:         script.setAttribute('language','javascript');
  94:         script.setAttribute('src','Library/js/' + clsPath + '.js');
  95:         head.appendChild(script);
  96:     }
  97: }
  98:  

(Before YUI Compressor)

   1: var IAT={};IAT.Eval=function(scr,scope){return eval(scr)};
   2: IAT.Importer=new function(){var a=this;var d=[];
   3: function c(){for(var j=0;j<d.length;j++){
   4: var h=d[j].refs;var l=[];for(var g=0;g<h.length;g++){
   5: if(b(h[g])){l.push(g)}}for(var g=l.length-1;g>=0;g--){
   6: h.splice(l[g],1)}if(d[j].refs.length<1){
   7: var f=d[j];d.splice(j,1);e(f);break}}}
   8: function e(f){IAT.Eval(f.clsName+"=scope.cls();",f);c()}
   9: function b(i,f){if(!f){f="window"}var h=i.split(".");
  10: var g=f+"."+h[0];if(!IAT.Eval(g)){return false}
  11: if(h.length==1){return true}h.splice(0,1);i=h.join(".");
  12: if(h.length>1){i=i.substring(0,i.length)}return b(i,g)}
  13: a.Ns=function(i,f){if(!f){f="window"}var h=i.split(".");
  14: var g=f+"."+h[0];if(!IAT.Eval(g)){IAT.Eval(g+"={};")}
  15: if(h.length==1){return}h.splice(0,1);i=h.join(".");
  16: if(h.length>1){i=i.substring(0,i.length)}this.Ns(i,g)};
  17: a.DefineClass=function(f){
  18: var h=f.clsName.substring(0,f.clsName.lastIndexOf("."));
  19: a.Ns(h);d.push(f);for(var g=0;g<f.refs.length;g++){
  20: a.Import(f.refs[g])}c()};a.Import=function(i){
  21: if(b(i)){return}var j=i.split(".");
  22: j.splice(0,1);var f=j.join("/");
  23: f=f.substring(0,f.length);var h=document.getElementsByTagName("head")[0];
  24: var g=document.createElement("script");
  25: g.setAttribute("type","text/javascript");
  26: g.setAttribute("language","javascript");
  27: g.setAttribute("src","Library/js/"+f+".js");h.appendChild(g)}};
  28:  

(After YUI Compressor - Return lines added for readability - Over a 25% savings!)

The character length drops from 1788 before compression to 1317 after. Any additional code that is added to this class not containing eval will only increase the savings percentage; making your users' downloading experience that much snappier!

Filed under: , ,
Changing Maximum Upload Size in WSS 3.0 and MOSS 2007
19 February 09 08:28 AM | jacobl | 0 Comments   

tmp22FLooking around the web, I found many articles describing how to do change the Maximum Upload Size in SharePoint, but for whatever reason, the options in the instructions were not available on my SharePoint installation. Blog posts by Joel Oleson on File Name, Length, Size and Invalid Character Restrictions and Recommendations and this one by Ronnie Guha mention the "Configure Virtual Server Settings" area on the "Virtual Server List" page in Central Admin. Neither of these existed. After some poking around, here is the correct navigation for WSS 3.0 and MOSS 2007. I'm not sure when in the builds of WSS that this change occurred, so follow the set of directions apply to the options you have available.

  1. Open Central Administration
  2. Open the Application Management tab.
  3. Choose Web Application General Settings (take note that the url is /_admin/vsgeneralsettings.aspx - that appears to prove its legacy in Virtual Server).
  4. Scroll down until you find the section labeled Maximum Upload Size.
  5. Change away.

There is an element in the web.config called httpRuntime. The maxRequestLength attribute (defaulted to 51200) is traditionally used to change the maximum file upload size in ASP.NET web applications, but does not appear to have any effect on SharePoint.

I highly recommend you take a look at Joel's post on this topic to get an idea of how you should set that value based on your needs, but these instructions should take you the rest of the way.

Filed under: , ,
SharePoint Best Practices - San Diego
10 February 09 02:18 PM | jacobl | 0 Comments   

bpbook_jpg I have to start off by saying that this was the best conference I've been to. Maybe that doesn't say much since I've only been to a handful, but it has topped them all. Last year, I attended conferences such as AJAX World and Microsoft's ReMix in Boston and they were OK at best. The SharePoint Best Practices show brought many of the industry leaders together in an effort to spread knowledge to a broad range of people in a broad range of sectors. From developers and system integrators to project managers and CIOs, SharePoint Best Practices didn't just sell SharePoint to people (I think its user base is beyond that); it taught them how to use it, why they'd want to, and what to do to get the most out of it without going insane. SharePoint is a great platform and I look forward to my continued work on it.

My Agenda (Notable sessions)

My goal at this show was to learn as much as possible about deployment, permissions, testing, and WebParts (namely "What can I do with a WebPart that doesn't seem normal?"). Every session I went to was fantastic, so I'll only highlight the ones that stood out.

Best Practices for Developing Web Parts : Todd Bleeker

While I was at this presentation, I decided to open an IM window and beam all of my notes about the talk to my colleague, Dave Terrell. By the end of our conversation, we ended up buying a few copies of Todd's book, Developer's Guide to the Windows SharePoint Services v3 Platform.

Todd covered everything from packaging the WebPart into a solution as part of a feature to some of the cool things you can do with invisible web parts (I'll be exploring this more in a future post).

Automating your test environments for SharePoint development using Hyper-V : Ben Robb

Side note: On the last night, we ended up going to SharePoint by Day SharePint by Night and I played pool with Ben. I must confess that I am absolutely terrible at pool. It didn't help that Ben is quite good at the game. Maybe I was imagining it, but I think heard him say he played snooker semi-professionally. It was a good time.

During his talk, Ben discussed automating the provisioning of new SharePoint virtual machines for integration testing and development. One thing that we'll be adopting from this talk is the idea of a-new-machine-a-week. In this setup, every developer gets a fresh machine every Monday. This way, we know the machine is clean and there's no cruft that could cause issues in development.

Best Practices for Disposing SharePoint Objects : Todd Bleeker

Besides going over the basics of properly disposing your SPWeb and SPSite objects that you create, Todd highlighted a new tool from Microsoft called SPDisposeCheck. It has been less than 2 business days since I left San Diego and it is already part of our build process here. Hopefully Microsoft will continue to improve the tool (namely fix the -xml output parameter). It's a great find.

Best Practices for Unit testing on SharePoint : Francis Cheung

In an earlier talk in the conference, Francis discussed abstracting all SharePoint object model calls into Repository classes that made code easier to read and allowed your business logic to be tested outside of SharePoint. During this presentation, Francis explained how to test those repositories using TypeMock for SharePoint. It's a pay-for product, but is really the only way to get the job done (outside of mocking all of WSS yourself).

Secure Coding Practices for the Administrator : Maurice Prather

This talk should have been called, "How to determine whether or not your third party solution provider has good SharePoint programming practices". Maurice discussed how developers should be using CAS policies to specify what level of security their assemblies require, when to ask your vendor for more information about their GAC-deployed assemblies, and much more.

Using Elevated Privileges and Impersonation : Paul Schaeflein & Maurice Prather

Paul started off by discussing what it means to "run with elevated privileges" and its differences with "impersonating as system account (or other users)". I will post more on this topic in the coming days. These two concepts are very different and depend on what you are looking to do.

More to Come

This was an excellent conference and I got a lot out of it. This will surely shape future development. I will be posting more on each of these topics with how-tos and the like. If you want more information about any of these talks, post a comment to move them up my stack.

Installing SharePoint on Windows Vista
16 January 09 07:52 AM | jacobl | 1 Comments   

Check out this article on Bamboo Solutions to install Windows SharePoint Services 3.0 SP1 on Vista x64/x86.

images We use that here in our office for demo laptops. It allows us to get the best performance when demoing our SharePoint products at shows by letting us run SharePoint directly on the hardware instead of in a VM. This is definitely a case where two OSs are not better than one.

I don't recommend that you use this for your development setup. It is a good idea to develop against WSS since it locks you into a feature set that makes your code deployable to both WSS and MOSS. But by installing SharePoint in a VM, you can roll back with snapshots. In this sort of Vista+SharePoint environment, if SharePoint bombs, you have little luck getting back without a system restore.

Filed under: ,
Hristo Pavlov's Blog on AllowUnsafeUpdates in SharePoint
14 January 09 02:05 PM | jacobl | 0 Comments   

This is a great post. A lot of good research was done to get to the bottom of this complicated topic.

Check it out here: What You Need to Know About AllowUnsafeUpdates

Filed under:
SharePoint Saturday: Developing and Packaging a Third Party SharePoint Solution
12 January 09 10:34 AM | jacobl | 1 Comments   

SharePointSat2VA-emaillarge Here are some notes from my presentation at SharePoint Saturday this past weekend in Virginia Beach, VA. Included are my slides, code that I demo'd, and hopefully answers to all of the questions that people asked.

The Slides

Download my slides in PDF form for quick reference.

Download my slides as a QuickTime movie to view the presentation with all animations. Unfortunately, to keep the file size low, I had to sacrifice a lot of quality. I suggest you download both the PDF and movie to get the full effect.

The Code

The Feature that we worked on uses a toolkit called DotImage. If you're interested in using the feature for testing, feel free to get an evaluation license.

Download the source code.

Download the WSP file (you will need to get an evaluation license for DotImage)

SPWebConfigModification

Here are links to Dave Terrell's 6 part series on using the SPWebConfigModification object to edit the web config of a given Web Application in SharePoint.

  1. The Problem
  2. Take this for example...
  3. Time for some code
  4. Why doesn't this work?
  5. The Remedy
  6. Putting it to work

WSPBuilder

Check out my short blog post on WSPBuilder with instructions on how to use it in a NAnt build script.

SharePoint Solution Installer

I will post a more extensive article with instructions on how to use this product and some ideas on how it might be improved, but for now, you can find the SharePoint Solution installer on CodePlex.

AllowUnsafeUpdates

Check out Hristo Pavlov's blog on AllowUnsafeUpdates here.

My Presentation Setup

Picture 1Here's a little more detail on the setup I used in my presentation: the computers I was using, which ones were VMs, how I switched between them and my slides, what presentation software I used, and how I tied it all together.

SharePoint Box

Since SharePoint is a bit of a beast, I decided to run it on an external machine. It was running on a Sony Viao with Vista. WSS 3.0 was installed directly on Vista. I will post on that in a week or so explaining how that's possible with steps on getting it done yourself.

Visual Studio Machine

I prefer to run Visual Studio on a different machine that I run SharePoint. The primary reason for this is so that I can quickly revert the VM that's running SharePoint in my development cycle without worrying about losing work.

For my presentation, I ran Visual Studio on a virtual machine in VMWare Fusion for Mac running Windows XP Pro.

Tying them Together

Since there was no guarantee of wireless offered at the Advanced Technology Center, I made sure I was all set by hosting a wireless network from my Mac and connecting to it from the Vista laptop. From there, I was able to remote in and get to SharePoint.

Presentation Software

I enjoy using Apple's Keynote software. It lets me make very attractive slides and is very easy to use. Plus, it exports to many formats that anyone can read without losing anything like transitions or animations.

Switching between Everything

To switch between my slides and my 2 demo machines, I used a built-in Mac feature called Spaces. It's a beautiful implementation of multiple-desktops.

Picture 2

WSPBuilder: Generate SharePoint Solution Files
12 January 09 07:40 AM | jacobl | 1 Comments   

There's a project on CodePlex called WSPBuilder. It's a console application that gets you from a given folder structure to a SharePoint Solution file. It would be nice if it was a NAnt / MSBuild task, but since it's an exe, calling it from your build script is trivial already.

Folder Structure

12
    TEMPLATE
        CONTROLTEMPLATES
        FEATURES
        LAYOUTS
        XML
    (anything you want to go in the 12 hive. just add any other folders)
GAC
    (any assemblies you would like deployed to the GAC)
80
    BIN
    (anything you want to go in the web application's virtual directory)
solutionid.txt

Sample Console Call from NAnt

<exec program="C:\Path\to\WSPBuilder.exe" commandline="-DLLReferencePath GAC -WSPName OutputSolutionFileName.wsp -TraceLevel Verbose" workingdir="C:\Path\to\Solution\Folder\Structure" />
 
KeepAlive for the DotImage WebThumbnailViewer
09 January 09 08:20 AM | jacobl | 0 Comments   

This is my third post on the topic of using a simple JavaScript call to keep a server session alive. My first post was on keeping a generic IIS session alive by reloading a hidden GIF, my second was related directly to keeping session state alive for the WebImageViewer, and this post will address the WebThumbnailViewer.

Using the WebThumbnailViewer with the WebImageViewer

If you're interested in keeping a session alive for a page that contains both a WebImageViewer and a WebThumbnailViewer working in tandem, you only need to keep the session for the WebImageViewer alive since they share the same stamp file in the AtalaCache. However, if you are using just the WebThumbnailViewer, this blog is for you.

Server Side

Luckily, you need not make any change to your code-behind. There is something very simple that we can call in DotImage on the client-side that will do the job for us.

Client Side

This is super simple. WebThumbnailViewer has an UpdateThumb call that does a round trip to the server, requests a thumb, and serves it. This causes the state keepalive that we are looking for.

   1: var keepAliveIframe = null;
   2: function KeepAlive(){
   3:     WebThumbnailViewer1.UpdateThumb(0);
   4: }
   5: setInterval('KeepAlive();', '900000');

That's it. Just add that function to your page and you'll be all set. Don't forget to update the setInterval duration to something that makes sense for your deployments. 900,000 milliseconds is 15 minutes so that should work in most cases.

SharePoint Check Out Policy Honoring
07 January 09 07:17 AM | jacobl | 0 Comments   

imageWhen writing a piece of Document Imaging software for SharePoint (such as VizitSP), it's likely that you'll need to worry about saving changes to files. This post will explore the different options there are and what compromises need to be made when deciding how to honor a SharePoint list's check out policy.

What Would Microsoft Do?

In almost every case, we tend to ask ourselves, "What would Microsoft do?". The debate as to whether a decision they made is the right one can be made elsewhere. The point is, Microsoft decided to do something a certain way, and now all of your customers are used to that behavior - right or not.

image As far as document editing tools go, Microsoft Office is the only piece of software that is integrated into SharePoint out-of-the-box. When viewing a Document Library that has Microsoft Word documents in it, you can easily choose to Edit the doc directly in Word. When you're done editing, you can save it directly back into SharePoint from Office and continue browsing without downloading anything. Vizit is very similar, but from the raster-image side of the fence. We need to make sure that we behave as much like Office as possible; checking files out when you'd expect them to be checked out, checking them in when it makes sense, and offering the same menus and options as you would see when editing Word documents.

A List by Another Name

Many would argue that Document Libraries are simply an extension of the Generic List. In almost every respect they would be correct. They behave the same, they both contain list items and can have "folders", and they both live in Sites. They place where they differ is in two critical areas regarding documents. Association and Versioning.

Items in a Document Library have a one-to-one correspondence with an SPFile. SPListItem.File is a direct pointer to the document in question. Items in a Generic List have a one-to-many correspondence with its attachments. SPListItem.Attachments is an SPAttachmentCollection (a glorified string array) that contains the names of the files attached to it. Recently, I posted a blog in how to get at list item attachments. Looks like Microsoft glossed over this one.

Document Libraries offer a versioning feature. Files can be checked out, checked in, published, etc. List Item attachments in Lists are missing the UI to do this. You can't check out, check in, publish. If that was the intended behavior, it would be nice if the object model reflected that decision. Unfortunately, the same object is used to represent a List Item attachment as is used to represent a file in a Document Library. Through the object model, you can check attachments out, check them in, etc. This is inconsistent at best. If you aren't aware that this is how things work, you are likely to put SharePoint into a state that's not trivial to get out of.

Why Can't I Delete this Attachment?

From the little bit of testing that I did, I discovered that attachment exhibit a strange behavior when they are checked out. From the SharePoint web page, you have no way of knowing that a given attachment is checked out. If you attempt to delete an attachment, it will seemingly go away. But, by simply refreshing the Edit Item page, the attachments reappear. Nothing was deleted!

The only way I was able to successfully delete an attachment that was checked out through the object model was to delete the entire item it was attached to. I didn't look to see if the attachment was actually deleted via the object model, but my guess is that it is still in there... but with nothing pointing to it. Not cool.

What to Do?

Seemingly, the best question we can ask ourselves is "What Would Microsoft Do?". Fortunately, they punted on this problem as well, not offering Office integration with List Item attachments. If you want to edit an attachment, you must download it, edit it, and reattach it, hoping that no one else is doing the same thing. Otherwise, it's a race to see who gets there last and steps on the other's changes.

One-Uping Microsoft

imageDoing Microsoft one better isn't too complicated in this case. We can offer users the ability to one-click edit raster images through VizitSP while maintaining the checkoutless model they provide for attachments. There's little we can do in this case until Microsoft offers the UI to check attachments back in after an aborted edit.

For those of you out there that would rather deal with the occasional support call asking how to get them back to normal, it is fairly easy to add some UI to your own app that lets them check the file back in. Just hope and pray they don't uninstall your app before they phone in.

Filed under:
Getting SharePoint to Tell You More about that "Unexpected Error"
05 January 09 07:05 AM | jacobl | 0 Comments   

If you're developing for SharePoint, you've undoubtedly encountered the oh-so-vague Unexpected Error. This blog will show you what changes need to be made to the web.config to make SharePoint tell you more.

Which Web.Config?

To find out which web config you should modify depends on your setup. Chances are, you can simply modify the one located in the root of the virtual directory associated with the web application that's causing the problems. To get there, follow these steps:

  1. Open Windows Explorer.
  2. Navigate to C:\inetpub\wwwroot\wss\Virtual Directories\
  3. Here are all the web applications you have in IIS. Choose the one that's name corresponds to the port number of the web app you're looking for.
  4. Open said web.config.

The Changes

There are four in total, and here they are:

  1. Search for the word "callstack". The line will look like this:
    <SafeMode MaxControls="200" CallStack="false" DirectFileDependencies="10" TotalFileDependencies="50" AllowPageLevelTrace="true">
     
    Set the value of the CallStack attribute to true.
  2. Search for the word "customerrors". That line looks like this:
    <customErrors mode="On" />
    Set the value of the mode attribute to Off or RemoteOnly. Hopefully, you are doing all of this debugging on a test server so turning custom errors off entirely shouldn't be a big deal. 
  3. Search for the word "batch". This last line looks like this:
    <compilation batch="false" debug="false">
    Set the value of the batch attribute and debug attribute to true.

Next

Now, go back to your page and hit refresh. You will see a much better error message and hopefully a stack trace pointing to your problem. If the Unexpected Error message didn't change, make sure you're web.config changes took hold by either resetting IIS entirely or recycling your application pool.

Cross Domain Remote Debugging
01 January 09 10:00 AM | jacobl | 2 Comments   

Remote debugging can be a task on it's own, but after the first time you've done it, it becomes second nature. This blog will take remote debugging a step further and explain step by step how to easily debug across domains.

Set Up Your VPN

imageIn Network Connections, use the "Create a new connection" wizard to set up the VPN.

You want to VPN into the same domain that your remote server is on. So for me, I found the IP address of the domain controller and connected to that.

When prompted for a username and password, use an account that is both a member of the domain and is part of the local Administrators group of the machine you are remotely debugging.

Placing the PDBs

General rule of thumb is that you want to put your PDBs directly next to their assemblies. While it's possible to follow that rule for assemblies that are in the GAC, it's much easier to put it in the "C:\Windows\Symbols\dll" folder. As usual, your PDBs must be built with the DLLs that you're using. If they aren't, nothing will work. Also, in the case of IIS, simply replacing the assemblies in the GAC will not force your web application to pick up the new assemblies. You will need to either recycle your app pool or simply do an iisreset.

Setting up Remote Debugging

There are a few ways to prepare your machines for remote debugging. If you plan on doing it more often than just this one time, I suggest you download and install Visual Studio to your remote machine. You'll then be able to install the Visual Studio 2008 Remote Debugger service which can be set to start on login. Then, you'll never have to think about it again.

If this is just a quick test, copy the appropriate Remote Debugger from your local Visual Studio box to the remote machine. See this MSDN article to find out where those files are located: How to: Set Up Remote Debugging. You need to copy all of the files in that directory so in the case of x86 remote debugging, copy the entire x86 directory over to your remote machine. Mine were located here:

   1: C:\Program Files\Microsoft Visual Studio 9.0\Common7\IDE\Remote Debugger\x86

Starting the app will look something like this:

image 

Make a note of the quoted server name. You will need that next.

On your Local Machine

imageOn your local machine, open up Visual Studio and choose "Debug > Attach to Process". Before you type in the server name into the Qualifier box, click "Select..." next to the "Attach to:" field. Check the code types you would like to debug and click OK.

Next, enter the Qualifier (this is the server name from before). It should look something like this: username@server. Hit Enter and wait.

image

If everything else went well, you will see a list of available processes for you to attach to.

Which process one do I pick?

You can select multiple processes if you're not sure which one is hosting your code. In the case of web applications, there can be multiple w3wp.exe processes available and it's nearly impossible to know which one applies. My only warning is this: do NOT select too many or you will have to wait for the polar ice caps to melt before your machine breaks free from Visual Studio's hold.... well, maybe that's a bad example.

Enjoy

This process can be a bit painful, but like "regular" remote debugging, it's not that bad the second time around. Fortunately, the result is well worth the work. Even though I've found it to be a little flaky, most of the regular Debugging features work as expected: Immediate Window, Intellisense, Break Points, Call Stack, etc.

KeepAlive for the DotImage WebImageViewer
31 December 08 08:22 AM | jacobl | 1 Comments   

Recently, I posted an article describing a simple way to keep a session alive during long periods of inactivity. That works great if your web application doesn't rely on some other information on the server to maintain state. If you use DotImage in your web app, it maintains certain state information in the AtalaCache that needs to be updated every so often. If not, you run the risk of another user hitting the page and causing the AtalaCache to be cleaned up (assuming the cache has expired). This post explains how to avoid this by forcing the WebImageViewer to update its timestamps on your state.

Server Side

In your page's code behind, add the following RemoteInvokable function:

   1: [RemoteInvokable]
   2: public void Remote_KeepSessionAlive()
   3: {
   4:     // Don't need to do anything here.
   5:     // A simple page request will force the
   6:     // WebImageViewer to update any timestamps 
   7:     // it needs for this session.
   8: }

Client Side

In your aspx page, add the following JavaScript:

   1: function KeepAlive(){
   2:     WebImageViewer1.RemoteInvoke('Remote_KeepSessionAlive');
   3: }
   4: setInterval('KeepAlive();', '900000');

We don't need to pass anything into the RemoteInvoke and we don't need to set an event handler for the return since it doesn't matter to us. Very simple.

This RemoteInvoke-based keepalive for the DotImage WebImageViewer will fire every 15 minutes (or 900,000 milliseconds == half of my session lifetime). Make sure you adjust that duration according to your server's session. Be sure to set it to something less than your session timeout or you might miss it.

Download Attachments from SharePoint ListItems
29 December 08 08:23 AM | jacobl | 2 Comments   

There are times when you might want to download an attachment from a SharePoint list item. In the case of Document Libraries, this is trivial because there is a File property associated with the SPItem that you can use to get access to the file. Unfortunately, the only information available to you about list item attachments is the SPAttachmentsCollection which is simply a list of strings (the names of the files). This is less than useful since it provides you with no information as to where those files might be.

My First Attempt

After a bit of poking around the object model, I finally gave up and came up with this:

   1: SPFolder folder = web.Folders["Lists"].SubFolders[list.RootFolder.Name].SubFolders["Attachments"].SubFolders[item.ID.ToString()];

Pretty insane, huh? I thought it was so terrible that I decided it was wrong and did some more research.

Research

First, I came across this blog which only disappointed me since Eric Shupps came up with the same thing. Even worse, the next result in my Google search was MSDN. Low and behold: How to Programmatically Download Attachments from List Items in Windows SharePoint Services. The hack is the official way to do things apparently.

Granted, it works fine. It's just not as elegant as it probably should be.

Another Problem

There is a problem that this still poses. You cannot enumerate through folders that you do not have Contributor access to. Users that only have View access will not be able to download files that they clearly have rights to download. Here's my fix for that situation:

   1: private SPFolder GetAttachmentsFolder(SPWeb web, SPListItem item)
   2: {
   3:     SPFolder folder = null;
   4:     SPSecurity.RunWithElevatedPrivileges(delegate()
   5:     {
   6:         using (SPSite elevatedSite = new SPSite(web.Site.ID))
   7:         using (SPWeb elevatedWeb = elevatedSite.OpenWeb(web.ID))
   8:         {
   9:             folder = elevatedWeb.Folders["Lists"].SubFolders[item.ParentList.RootFolder.Name].SubFolders["Attachments"].SubFolders[item.ID.ToString()];
  10:         }
  11:     });
  12:     return folder;
  13: }

You can then get the ith attachment from the folder. Since your call to folder.Files[index or file name] will not be in an elevated call, SharePoint will do the right thing and block unauthorized accesses as expected.

Custom NAnt Tasks: TFSCheckOut and TFSCheckIn
24 December 08 10:50 AM | jacobl | 0 Comments   

Here are a few custom NAnt tasks we use in-house for our TFS needs: tfscheckout, tfscheckin, tfsmerge, and a few others. I posted on the topic of Writing Custom NAnt Tasks a while ago, and some of them have been updated. So, if you are using those, make sure you update your build scripts to use the modified schema.

Below is a list of some of the included tasks and how you might use them.

TFSCheckOut

   1:  
   2: <tfscheckout server="tfsServerName"
   3:              ssl="false"
   4:              port="8080"
   5:              projectPath="$/Path/to/Project/In/TFS"
   6:              localPath="C:\path\to\workspace\dir"
   7:              recursionType="Full"
   8:              workspace="nameOfWorkspace"
   9:              deleteWorkspace="false">
  10:   <fileset>
  11:     <include name="C:\Path\to\file\in\workspace"  />
  12:     <include name="C:\Path\to\another\file\in\workspace"  />
  13:   </fileset>
  14: </tfscheckout>

TFSCheckIn

   1: <tfscheckin server="tfsServerName"
   2:          ssl="false"
   3:          port="8080"
   4:          projectPath="$/Path/to/Project/In/TFS"
   5:          localPath="C:\path\to\workspace\dir"
   6:          recursionType="Full"
   7:          comment="NAnt Checkin"
   8:          workspace="nameOfWorkspace"
   9:          deleteWorkspace="false">
  10:   <fileset>
  11:     <include name="C:\Path\to\file\in\workspace\that\is\checked\out" />
  12:     <include name="C:\Path\to\another\file\in\workspace\that\is\checked\out" />
  13:   </fileset>
  14: </tfscheckin>

TFSMerge

   1: <tfsmerge server="atalatfs"
   2:          ssl="false"
   3:          port="8080"
   4:          projectPath="${ProjectPath}"
   5:          localPath="${Atala::GetCWD()}"
   6:          comment="NAnt Library Merge"
   7:          workspace="${Server}"
   8:          deleteWorkspace="false"
   9:          force="false">
  10:   <merges>
  11:     <merge from="$/Path/to/file/in/tfs" to="$/Path/to/branched/file" />
  12:     <merge from="$/Path/to/another/file/in/tfs" to="$/Path/to/this/files/branched/copy" />
  13:   </merges>
  14: </tfsmerge>

Also Included

Other tasks that are included in this assembly are TFSGet, AddXMLElement, RemoveXMLElements, RemoveTFSBindings, and LaunchDebugger.

Download

More Posts Next page »