Question

Is it possible in SharePoint to create a custom upload page that combines the file upload control with custom field types so that a user can select a file to upload from their hard drive, enter a title for the file, optionally add comments, specify the content type and enter additional data into several custom fields and have the new SPListItem created, the file uploaded and associated to the new SPListItem and finally have all the values entered into the custom fields successfully saved to the newly created SPListItem?

NOTE: I am looking to perform this tasks ONLY using SharePoint custom field types and NOT simply using a custom ASPX page with a bunch of UserControls.

The underlying issue that exists when using custom field types is that a file upload event in a SharePoint Document Library is an asynchronous event. You may override the default behavior of the ItemAdding method that is available in a SPListItemEventReceiver that can be used to access certain information when a file is "in the act of being uploaded" and you can likewise access information about the newly created SPListItem from the ItemAdded method that is called "after an item has already been added" -but since this method occurs in a separate thread and is executed ASYNCHRONOUSLY with no knowledge of anything related to the custom fields or their respective values, none of the data entered by the user in those fields is ever successfully saved.

When a user wishes to UPDATE information about a document by editing the values in custom fields using the EditFormTemplate the SPListItem property for each field is set during initialization. This all works fine because in such a case the ListItem already exists! The problem is that when a user wishes to upload a document for the first time, the ListItem obviously doesn't exist yet so each of the fields are initialized with the SPListItem property set to "null" and will forever remain null because there just doesn't seem to be any method for retroactively updating each field's ListItem property with a reference to the newly created ListItem AFTER the file is uploaded!

It is for this reason and this reason alone why Microsoft insisted on forcing users to upload their file(s) on one screen and then redirect them to the Edit Form after the file is successfully uploaded. By splitting apart the two pages, Microsoft forces the user to upload the file and create the ListItem PRIOR to saving any other information about the file. Once the file is uploaded and the ListItem is created there are no issues with saving each of the individual custom field's values back to the ListItem because the ListItem already exists!

NOTE: BaseFieldControl inherits from FieldMetadata, which inherits from FormComponent. FormComponent has a property called Item which corresponds to the underlying SPListItem that the field belongs to. BaseFieldControl has a property called ListItemFieldValue that stores the actual value of the field that is saved back to the ListItem and it also has an overridable method called UpdateFieldValueInItem() that may be used to perform additional logic (such as validation) prior to assigning data to the ItemFieldValue property.

When UPDATING an EXISTING SPListItem, the following code is valid and the custom field values will be saved because the SPListItem already exists!

var item = MyDocLib.Items[0] as SPListItem;
item["MyCustomFieldName"] = "some value";
item.Update();

In an SPListItemEventReceiver, during the initial file upload, after the ListItem has been created and the individual custom field values "attempt to get saved" the ItemUpdating/ItemUpdated methods will contain null references for the SPItemEventProperties properties.ListItem because, as mentioned previously, the ItemAdded method is fired asynchronously and the newly created ListItem is unavailable in the ItemUpdating/ItemUpdated methods.

Was it helpful?

Solution 2

OK, so creating a custom upload form which combines a file upload input control AND custom fields from a library with one or more OOTB or custom SPListFieldIterators is NOT an easy task -which is probably why Microsoft decided to seperate the process into two distinct and completely unrelated operations.

Nevertheless, there is an inherent value in allowing this sort of functionality as it enables users to simultaneously upload a file and persist metadata in a single atomic operation so that there never exists a document in your library that is sitting out there in the "ether" without any identifying information.

So what did it take? Several things.

The first was creating a utility class which I called "FileUploader" and this is what it looks like.

public class FileUploader
{
    #region Fields

    private readonly SPList list;
    private readonly FileUpload fileUpload;
    private string contentTypeId;
    private string folder;
    private SPContext itemContext;
    private int itemId;

    #endregion

    #region Properties

    public bool IsUploaded
    {
        get
        {
            return this.itemId > 0;
        }
    }

    public SPContext ItemContext
    {
        get
        {
            return this.itemContext;
        }
    }

    public int ItemId
    {
        get
        {
            return this.itemId;
        }
    }

    public string Folder
    {
        get
        {
            return this.folder;
        }

        set
        {
            this.folder = value;
        }
    }

    public string ContentTypeId
    {
        get
        {
            return this.contentTypeId;
        }

        set
        {
            this.contentTypeId = value;
        }
    }

    #endregion

    public FileUploader(SPList list, FileUpload fileUpload, string contentTypeId)
    {
        this.list = list;
        this.fileUpload = fileUpload;
        this.contentTypeId = contentTypeId;
    }

    public FileUploader(SPList list, FileUpload fileUpload, string contentTypeId, string folder)
    {
        this.list = list;
        this.fileUpload = fileUpload;
        this.contentTypeId = contentTypeId;
        this.folder = folder;
    }

    public event EventHandler FileUploading;
    public event EventHandler FileUploaded;

    public event EventHandler ItemSaving;
    public event EventHandler ItemSaved;

    public void ResetItemContext()
    {
        //This part here is VERY, VERY important!!!
        //This is where you "trick/hack" the SPContext by setting it's mode to "edit" instead
        //of "new" which gives you the ability to essentially initialize the
        //SPContext.Current.ListItem and set it's ItemId value. This of course could not have
        //been accomplished before because in "new" mode there is no ListItem. 
        //Once you've done all that then you can set the FileUpload.itemContext 
        //equal to the SPContext.Current.ItemContext. 
        if (this.IsUploaded)
        {
            SPContext.Current.FormContext.SetFormMode(SPControlMode.Edit, true);
            SPContext.Current.ResetItem();
            SPContext.Current.ItemId = itemId;

            this.itemContext = SPContext.Current;
        }
    }

    public bool TryRedirect()
    {
        try
        {
            if (this.itemContext != null && this.itemContext.Item != null)
            {
                return SPUtility.Redirect(this.ItemContext.RootFolderUrl, SPRedirectFlags.UseSource, HttpContext.Current);
            }
        }
        catch (Exception ex)
        {
            // do something
            throw ex;
        }
        finally
        {
        }

        return false;

    }

    public bool TrySaveItem(bool uploadMode, string comments)
    {
        bool saved = false;
        try
        {
            if (this.IsUploaded)
            {
                //The SaveButton has a static method called "SaveItem()" which you can use
                //to kick the whole save process into high gear. Just right-click the method
                //in Visuak Studio and select "Go to Definition" in the context menu to see
                //all of the juicy details.
                saved = SaveButton.SaveItem(this.ItemContext, uploadMode, comments);

                if (saved)
                {
                    this.OnItemSaved();
                }
            }
        }
        catch (Exception ex)
        {
            // do something
            throw ex;
        }
        finally
        {
        }

        return saved;
    }

    public bool TrySaveFile()
    {
        if (this.fileUpload.HasFile)
        {
            using (Stream uploadStream = this.fileUpload.FileContent)
            {
                this.OnFileUploading();

                var originalFileName = this.fileUpload.FileName;

                SPFile file = UploadFile(originalFileName, uploadStream);

                var extension = Path.GetExtension(this.fileUpload.FileName);

                this.itemId = file.Item.ID;

                using (new EventFiringScope())
                {
                    file.Item[SPBuiltInFieldId.ContentTypeId] = this.ContentTypeId;
                    file.Item.SystemUpdate(false);

                    //This code is used to guarantee that the file has a unique name.
                    var newFileName = String.Format("File{0}{1}", this.itemId, extension);

                    Folder = GetTargetFolder(file.Item);

                    if (!String.IsNullOrEmpty(Folder))
                    {
                        file.MoveTo(String.Format("{0}/{1}", Folder, newFileName));
                    }

                    file.Item.SystemUpdate(false);
                }

                this.ResetItemContext();

                this.itemContext = SPContext.GetContext(HttpContext.Current, this.itemId, list.ID, list.ParentWeb);
                this.OnFileUploaded();

                return true;
            }
        }

        return false;
    }

    public bool TryDeleteItem()
    {
        if (this.itemContext != null && this.itemContext.Item != null)
        {
            this.ItemContext.Item.Delete();

            return true;
        }

        return false;
    }

    private SPFile UploadFile(string fileName, Stream uploadStream)
    {
        SPList list = SPContext.Current.List;

        if (list == null)
        {
            throw new InvalidOperationException("The list or root folder is not specified.");
        }

        SPWeb web = SPContext.Current.Web;

        SPFile file = list.RootFolder.Files.Add(fileName, uploadStream, true);

        return file;
    }

    private string GetTargetFolder(SPListItem item)
    {
        var web = item.Web;
        var rootFolder = item.ParentList.RootFolder.ServerRelativeUrl;
        var subFolder = GetSubFolderBasedOnContentType(item[SPBuiltInFieldId.ContentTypeId]);

        var folderPath = String.Format(@"{0}/{1}", rootFolder, subFolder);
        var fileFolder = web.GetFolder(folderPath);

        if (fileFolder.Exists) return folderPath;

        return Folder;
    }

    private void OnFileUploading()
    {
        EventHandler handler = this.FileUploading;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void OnFileUploaded()
    {
        EventHandler handler = this.FileUploaded;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void OnItemSaving()
    {
        EventHandler handler = this.ItemSaving;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }

    private void OnItemSaved()
    {
        EventHandler handler = this.ItemSaved;
        if (handler != null)
        {
            handler(this, EventArgs.Empty);
        }
    }
}

Then I used it in my "CustomUpload" class that is the CodeBehind for my ASPX page.

public partial class CustomUpload : LayoutsPageBase
{
    #region Fields

    private FileUploader uploader;

    #endregion

    #region Properties

    public SPListItem CurrentItem { get; set; }
    public SPContentType ContentType { get; set; }
    public int DocumentID { get; set; }

    private SPList List;

    #endregion

    public CustomUpload()
    {
        SPContext.Current.FormContext.SetFormMode(SPControlMode.New, true);
    }

    protected override void OnInit(EventArgs e)
    {
        if (IsPostBack)
        {
            // Get content type id from query string.
            string contentTypeId = this.Request.QueryString["ContentTypeId"];
            string folder = this.Request.QueryString["RootFolder"];

            //ALL THE MAGIC HAPPENS HERE!!!
            this.uploader = new FileUploader(SPContext.Current.List, this.NewFileUpload, contentTypeId, folder);

            //These event handlers are CRITIAL! They are what enables you to perform the file
            //upload, get the newly created ListItem, DocumentID and MOST IMPORTANTLY...
            //the newly initialized ItemContext!!!
            this.uploader.FileUploading += this.OnFileUploading;
            this.uploader.FileUploaded += this.OnFileUploaded;
            this.uploader.ItemSaving += this.OnItemSaving;
            this.uploader.ItemSaved += this.OnItemSaved;
            this.uploader.TrySaveFile();
        }

        base.OnInit(e);
    }

    protected void Page_Load(object sender, EventArgs e)
    {
        //put in whatever custom code you want...
    }

    protected void OnSaveClicked(object sender, EventArgs e)
    {
        this.Validate();

        var comments = Comments.Text;

        if (this.IsValid && this.uploader.TrySaveItem(true, comments))
        {
            this.uploader.TryRedirect();
        }
        else
        {
            this.uploader.TryDeleteItem();
        }
    }

    private void OnFileUploading(object sender, EventArgs e)
    {
    }

    private void OnFileUploaded(object sender, EventArgs e)
    {
        //This is the next VERY CRITICAL piece of code!!!
        //You need to retrieve a reference to the ItemContext that is created in the FileUploader
        //class and then set your SPListFieldIterator's ItemContext equal to it.
        this.MyListFieldIterator.ItemContext = this.uploader.ItemContext;

        ContentType = this.uploader.ItemContext.ListItem.ContentType;

        this.uploader.ItemContext.FormContext.SetFormMode(SPControlMode.Edit, true);
    }

    private void OnItemSaving(object sender, EventArgs e)
    {
    }

    private void OnItemSaved(object sender, EventArgs e)
    {
        using (new EventFiringScope())
        {
            //This is where you could technically set any values for the ListItem that are
            //not tied into any of your custom fields.
            this.uploader.ItemContext.ListItem.SystemUpdate(false);
        }
    }
}

OK...so what is the gist of all this code?

Well if you aren't keen on actually looking at the comments I provided I will give you a brief summary.

Essentially what the code does is perform the whole file upload process using the FileUploader helper class and uses a series of EventHandlers that are attached to various SPItem and SPFile related events (i.e. saving/saved and uploading/uploaded) which allow the newly created SPListItem and ItemContext objects and the SPListItem.Id value to be synchronized with the SPContext.Current.ItemContext being used in the CustomUpload class. Once you have a valid and newly refreshed ItemContext you can "sneaky deaky" set the existing ItemContext that is being used by your SPListFieldIterator (which is managing your custom fields) equal to the ItemContext that was created in the FileUpload class and passed back to the CustomUpload class that actually has a reference to the newly created ListItem!!!

The additional thing to note here is that you need to set the control mode for both the SPContext.Current.FormContext and the SPListFieldIterator from "New" to "Edit". If you don't do that then you won't be able to set the ItemContext and ListItem properties and your data won't be saved. You also can't start off by setting the control mode value to "Edit" because then the FormContext and SPListFieldIterator will be expecting an existing ItemContext which of course will not exist during any point in the intitial page or control life cycle because you haven't actually uploaded the file yet!!!

All of the above code MUST execute from the OnInit method of the CustomUpload class. The reason for this is so you can inject the newly created ItemContext into your SPListFieldIterator before it initializes itself AND it's child SPField controls (i.e. YOUR CUSTOM CONTROLS!!!). Once the SPListFieldIterator has a reference to the newly created ItemContext it can then initialize all of it's child SPField controls with said ItemContext and THAT is how you can use a custom upload page that merges a FileUpload control with custom fields along with one or more SPListFieldIterators that successfully uploads the file AND saves all of the values from your custom fields in a single atomic operation!

DONE AND DONE!

NOTE: This solution is not "technically" a single or atmoic operation but it gets the job done.

OTHER TIPS

For uploading files and linking them with list items, you can use Sparqube Document field type. Note: It is commercial add-on.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top