Why doesn't this Write Delagate work?

Feb 7, 2011 at 2:52 AM

Okay I have been over and over this all weekend and this darn thing just won't work. Here's all the code that I think matters.

 

What happens is that I get the zip file, but the file is totally empty. It has the filenames, but they are 0 bytes. Why be this?

 

        /// <summary>
        /// Outputs the zip file stream to the response output stream
        /// </summary>
        /// <param name="docIDs">An array of document id's to include in the zip file.</param>
        private void SendZipResponse(int[] docIDs)
        {
            SPSite site = SPContext.Current.Site;
            Response.Clear();
            //set response parameters for the zip attachment
            Response.Buffer = false;
            Response.Expires = 0;
            Response.ContentType = "application/x-zip-compressed";
            String guid =Guid.NewGuid().ToString();
            string disHeader = string.Format("Attachment;Filename=\"download-{0}.zip\"", guid);
            Response.AppendHeader("Content-Disposition", disHeader);

            //create a lock as the zip output stream is not thread safe
            lock (lockObj)
            {
                try{
                    using (ZipFile zip = new ZipFile())
                    {
                        hs = new Hashtable(docIDs.Length);

                        foreach (int docID in docIDs)
                        {
                            //get a managed file for the binary file data 
                            //and metadata
                            ManagedFile mf = new ManagedFile(site);
                            mf.ID = docID;
                            mf.Load();
                            //get a bytes array from the document                                                 

                            hs.Add(Path.GetFileName(mf.URL), mf);
                            zip.AddEntry(Path.GetFileName(mf.URL), WriteEntry);
                                          
                        }
                        zip.Save(Response.OutputStream);
                        Response.OutputStream.Flush();
                    }
                }
                catch (Exception ex){
                        Console.Out.Write("storp");
                    }
         
            Response.Close();

            }
        }

        private void WriteEntry(String filename, Stream output)
        {
            ManagedFile value = (ManagedFile)hs[filename];
            output = new MemoryStream(value.GetBinary());

        }

 

and then in the ManagedFile class:

 

    /// <summary>
        /// Returns a byte array of the underlying file data
        /// </summary>
        /// <returns>An array of bytes</returns>
        public byte[] GetBinary()
        {
            //open the web based on the managed file's web property
            using (SPWeb web = activeSite.OpenWeb(this.Web))
            {
                //get the item by the managed file's item property (guid).
                SPListItem item = web.Lists[this.List].Items[this.Item];

                //open the file creating a binary stream
                return item.File.OpenBinary();               
            }
        }

 

Coordinator
Feb 7, 2011 at 10:17 PM

in the WriteDelegate, you need to actually write the data to the output stream.   It is not sufficient to simply assign a value to the output stream variable, which is what your code does.

Like this:

        private void WriteEntry(String filename, Stream output)
        {
            ManagedFile value = (ManagedFile)hs[filename];
            byte[] b = value.GetBinary();
            output.Write(b,0,b.Length);
        }

 

Feb 8, 2011 at 1:57 PM

Yeap, that was the trick! Works like a charm now. Also by setting that buffer to FALSE, I get a nice effect of the Download starting right away and then just streaming along as it goes!

That probably seems silly, but we're using this function on our Organizational Meetings page and most of our users are non tech savy, so they need something to pop up right away or they think it's broken and they complain and I loose my job and my kids (2 boys, 6 and 4) go hungry! Well okay, maybe not that bad.

One last question, if I wanted to pass the docID variable to the WriteEntry method, so that I didn't have to have a member variable like "hs" defined, would I have to modify the DotNetZip source? or is there a way to provide an override without touching the DotNotZip DLL?

See how in the whole code I have to have a HashTable called "hs" defined at the class level so that I can get the ManagedFile I am interested in? I wish I could just pass the DocID variable to WriteEntry, but since it's a delegate the signature is set in stone it seems.

 

using System;
using System.Collections;
using System.IO;
using System.Web;
using System.Web.UI;
using Ionic.Zip;
using Microsoft.SharePoint;

namespace MISO.IR.ECM.SP.ApplicationPages
{
    /// 
    /// A page that zips a set of files based on query string parameters
    /// 
    public class ZipFiles : Page
    {
        Hashtable hs;
      //  private object lockObj = new object();

        /// 
        /// Overrides the base OnLoad method for the zip file response.
        /// 
        /// <param name="e" />Event Args
        protected override void OnLoad(EventArgs e)
        {
            base.OnLoad(e);

            //get the query string of document id's
            object queryString = Request.QueryString["docIDs"];

            //validate value exists
            if (queryString == null)
                return;

            //convert the dash '-' seperated string values to an integer array.
            int[] docIDs = GetDocIDs(queryString.ToString());

            //send the zip file back in the response as an attachment
            SendZipResponse(docIDs);            
        }

        /// 
        /// Outputs the zip file stream to the response output stream
        /// 
        /// <param name="docIDs" />An array of document id's to include in the zip file.
        private void SendZipResponse(int[] docIDs)
        {
            SPSite site = SPContext.Current.Site;
            Response.Clear();
            //set response parameters for the zip attachment
            Response.Buffer = false;
            Response.Expires = 0;
            Response.ContentType = "application/x-zip-compressed";
            String guid = Guid.NewGuid().ToString();
            string disHeader = string.Format("Attachment;Filename=\"download-{0}.zip\"", guid);
            Response.AppendHeader("Content-Disposition", disHeader);

            //create a lock as the zip output stream is not thread safe
            //lock (lockObj)
            //{
            using (ZipFile zip = new ZipFile())
            {
                hs = new Hashtable(docIDs.Length);
                foreach (int docID in docIDs)
                {
                    ManagedFile mf = new ManagedFile(site);
                    mf.ID = docID;
                    mf.Load();
                    hs.Add(Path.GetFileName(mf.URL), mf);
                    zip.AddEntry(Path.GetFileName(mf.URL), WriteEntry);
                }
                zip.Save(Response.OutputStream);
            }
            HttpContext.Current.ApplicationInstance.CompleteRequest();
        }

        private void WriteEntry(String filename,  Stream output)
        {
            ManagedFile value = (ManagedFile)hs[filename];
            byte[] b = value.GetBinary();
            output.Write(b, 0, b.Length);
        }

        /// 
        /// Converts a dash '-' seperated string of numbers to an integer array
        /// 
        /// <param name="queryString" />The string to parse.
        /// An array on integers
        private int[] GetDocIDs(string queryString)
        {
            //split the string into an array of number string values
            string[] docIDs = queryString.ToString().Split(new char[] { '-' }, StringSplitOptions.RemoveEmptyEntries);
            
            //create a matching sized integer array
            int[] retArray = new int[docIDs.Length];

            int intVal = 0;

            //loop through the string values parsing them into the integer array
            for (int i = 0; i < docIDs.Length; i++)
            {
                if (!int.TryParse(docIDs[i], out intVal))
                    throw new ArgumentException("Document ID's must be integer values.");

                retArray[i] = intVal;
            }

            return retArray;
        }
    }
}

Coordinator
Feb 14, 2011 at 3:24 AM
Edited Feb 14, 2011 at 3:28 AM

You can avoid making the Hashtable an instance variable by converting your WriteEntry to be an Action defined within SendZipResponse. For this you need at least .NET 3.5, I think.  An Action is basically a delegate, but it is defined within the SendZipResponse() method, and can be used only there (unless you pass it out via a separate call).  Because it is defined within the SendZipResponse() method, the WriteEntry action can access local variables that are in-scope.  Move your hashtable variable into that method, and the WriteEntry Action can access it, via closures. 

Also, I'd recommend using the generic Dictionary, instead of the Hashtable type. It gives you a typesafe hashtable.  For that you will need to add a "using System.Collections.Generic;" At the top of your source file.  And, I'd recommend Response.Close() in lieu of mumblefroo.CompleteRequest().

All those changes look like this in code: 


      private void SendZipResponse(int[] docIDs)
      {
          SPSite site = SPContext.Current.Site;
          var hs = new Dictionary<string,ManagedFile>();
          var WriteEntry = new Action<String,Stream>((filename,output) => {
                  byte[] b = hs[filename].GetBinary();
                  output.Write(b, 0, b.Length);
              });

          Response.Clear();
          //set response parameters for the zip attachment
          Response.Buffer = false;
          Response.Expires = 0;
          Response.ContentType = "application/x-zip-compressed";
          String guid = Guid.NewGuid().ToString();
          string disHeader = string.Format("Attachment;Filename=\"download-{0}.zip\"", guid);
          Response.AppendHeader("Content-Disposition", disHeader);

          //create a lock as the zip output stream is not thread safe
          //lock (lockObj)
          //{
          using (ZipFile zip = new ZipFile())
          {
              foreach (int docID in docIDs)
              {
                  ManagedFile mf = new ManagedFile(site);
                  mf.ID = docID;
                  mf.Load();
                  hs.Add(Path.GetFileName(mf.URL), mf);
                  zip.AddEntry(Path.GetFileName(mf.URL), WriteEntry);
              }
              zip.Save(Response.OutputStream);
          }
          HttpContext.Close();
      }


Feb 15, 2011 at 2:38 PM

Thanks for working so hard on this Cheeso!

Unfortunately this is on SharePoint 2007 install, so it's .Net 2.0 only! :(

I wasn't able to get the sample code working. Action<t> in 2.0 can only take the 1 parameter. I couldn't get the method generated as an Action delegate that made DotNetZip happy with WriteEntry having 2 parameters.

We did alot of testing and it seems that the Dictionary<string, managedfile> is proving very thread safe and VERY fast. We're planning to go with that.

 

Once it's done and in Production, I will share the link with you, Cheeso, to see your hard work in action!

Coordinator
Feb 15, 2011 at 4:31 PM

ok, well if you are using .NET 2.0, then you can use the WriteDelegate type. like this:

            Ionic.Zip.WriteDelegate WriteEntry = 
                new Ionic.Zip.WriteDelegate((filename,output) => {
                    byte[] b = hs[filename].Whatever...
                    output.Write(b, 0, b.Length);
                });
            

 I'm glad the Dictionary is working for you.

Anyway, good luck.