📜 ⬆️ ⬇️

Structure attachments to list items in Sharepoint 2010

Good day to all!

In Sharepoint 2010 there are many standard types of site columns, but at least two important ones, in my opinion, are missing. Perhaps the developers have left the "field" for extensions from third-party companies. But we "ourselves with a mustache" =)

Missing out of the box columns are of the type time and file type. For a date type column, you can select either a date with time, or a single date. But only the time in the column can not be stored. Implementing a control to edit two drop-down lists with possible values ​​for hours and minutes does not seem to be a very interesting task. Therefore, under the cat, the implementation of the file type field is SPFileField , which can also be used as a brief instruction on creating your own column types for Sharepoint 2010. The solution allows you to add new type columns to the Sharepoint lists and load files into them via automatically generated forms. At the same time, the files and information about them are “laid out” in different fields:


In general, the task looks like this: the user wants to create list items and attach files to them, and the attached files need to be expanded into different columns depending on their purpose (for example, technical task, payment contract, details, etc.) and display links to download them to other users. The idea of ​​implementation is as follows: store all files in the document library, and in the list there is only a link to the desired file from the library. The user should show the link for downloading / deleting / replacing the file (if any) and the form for adding a new file to the document library. When saving, process changes made on the client and, if necessary, update the document library.
')
Having looked now at the set of languages ​​and technologies involved in this implementation, it can not but rejoice that they are all represented in Visual Studio. Creating a fully working version required a description of the column type in XML, encoding of field logic and related controls in C #, client logic in JavaScript, description of the appearance of controls in ASCX and its style in CSS, as well as a pair of triggers, again in C #. Now about everything in order ...

XML column type description


To describe your own type of column, the first thing you need to do (after creating a project, of course) is to add a Sharepoint Mapped Folder to your project called XML. This can be done from the context menu of the project (the XML folder is located in the Template folder):

You can immediately add the following directories: Template / CONTROLTEMPLATES, Template / LAYOUTS / STYLES, Template / LAYOUTS / XSL. Here's what I got:

In order to define a new type of Sharepoint column, you need to add a new file of the appropriate extension to the XML folder. This file defines the properties of a new type, such as the name of the column type, the path to the class that describes the behavior, a list of additional properties that the user can set when creating the column, etc.
fldtypes_SPFileField.xml
<?xml version="1.0" encoding="utf-8" ?> <!--     ,      --> <FieldTypes> <!--   --> <FieldType> <!--       CAML--> <Field Name="TypeName">SPFileField</Field> <!--    ( /custom-) --> <Field Name="ParentType">Text</Field> <!--      SharePoint --> <Field Name="TypeDisplayName">SPFileField</Field> <!--       --> <Field Name="TypeShortDescription">SPFileField</Field> <!--     --> <Field Name="UserCreatable">TRUE</Field> <!-- ,      --> <Field Name="ShowOnListCreate">TRUE</Field> <Field Name="ShowOnSurveyCreate">TRUE</Field> <Field Name="ShowOnDocumentLibraryCreate">TRUE</Field> <Field Name="ShowOnColumnTemplateCreate">TRUE</Field> <!--,            --> <Field Name="Sortable">FALSE</Field> <Field Name="Filterable">FALSE</Field> <!--         ,        (Text) --> <Field Name="AllowBaseTypeRendering">TRUE</Field> <!--   ( )   --> <Field Name="FieldTypeClass">SPFileFieldControl.SPFileField, $SharePoint.Project.AssemblyFullName$</Field> <!-- ,       . --> <PropertySchema> <Fields> <!--      ,      ,   ,       --> <Field Name="LibraryName" DisplayName=" " Type="Text" Required="TRUE" /> </Fields> </PropertySchema> </FieldType> </FieldTypes> 

More details can be found here and further on the links. For testing, we will create a list ( TestList ) and a document library ( TestLibrary ). After the solution is deployed in its current form, the SPFileField column can be added to the TestList list (as well as to any other):

It is not necessary to activate anything, since the custom type does not count as a feature. If suddenly a new column type does not appear, restart IIS. After adding the SPFileField column, an error will pop up, since the specified type SPFileFieldControl.SPFileField was not found in the assembly. We will correct this by adding the file SPFileField.cs to the project, which will contain the description of the class of the new column (SPFileField), value class (FileValue) and control (SPFileFieldControl) responsible for displaying and editing.

C # Server Logic


FileValue is inherited from SPFieldMultiColumnValue and is programmed to store 3 properties: a file name, a file path and a unique file ID. SPFieldMultiColumnValue allows you to store these values ​​in a single line through the separator ' ; # ' and provides access to them through an indexer. The code is hidden under the spoiler, but here and further commented quite well.
Filevalue
  public class FileValue : SPFieldMultiColumnValue { private const int c_PropNumber = 3; public FileValue() : base(c_PropNumber) {} public FileValue(string value) : base(value) {} public string Name { get { return base[0]; } set { base[0] = value; } } public string Url { get { return base[1]; } set { base[1] = value; } } public Guid _UniqueID = Guid.Empty; public Guid UniqueID { get { if (_UniqueID == Guid.Empty) _UniqueID = new Guid(base[2]); return _UniqueID; } set { _UniqueID = value; base[2] = value.ToString(); } } } 

SPFileField is inherited from SPField, which is the base for Sharepoint list columns. In the SPFileField class, we will implement the necessary constructors for the parent class, we obtain the LibraryName property described above, and use it when creating the SPFileFieldControl, which will be responsible for displaying controls on the viewing and editing forms.
SPFileField
  public class SPFileField : SPField { //    SPField  public SPFileField(SPFieldCollection fields, string fieldName) : base(fields, fieldName) { } public SPFileField(SPFieldCollection fields, string typeName, string displayName) : base(fields, typeName, displayName) { } public override BaseFieldControl FieldRenderingControl { get { //     string libraryName = Convert.ToString(this.GetCustomProperty("LibraryName")); //         BaseFieldControl ctrl = new SPFileFieldControl(libraryName); ctrl.FieldName = this.InternalName; return ctrl; } } //     FileValue public override object GetFieldValue(string value) { if (String.IsNullOrEmpty(value)) return null; return new FileValue(value); } } 

And finally, the third class in the SPFileField.cs file is SPFileFieldControl , which describes the logic of the file loading operation. This class is inherited from BaseFieldControl. SPFileFieldControl contains a description of the template names that must be used to draw the field on the display and editing forms. The SetupEditTemplateControls and SetupDisplayTemplateControls functions describe setting up templates depending on whether a file has already been downloaded. In order not to be limited to one column of the new type SPFileField, the list in the SetupEditTemplateControls function dynamically hooks JavaScript to delete and add files, which use control IDs that go to the client machine.

Here is the logic for handling custom changes. It is described in the overloaded UpdateFieldValueInItem method and consists in the following: if the file was loaded (the ItemFieldValue field is specified), but the hidden field containing its UniqueID is empty, then the linked item from the document library will be deleted. Also, the linked item is deleted when a new file is loaded. Initially, there were attempts to replace the file without deleting the library item, but this did not lead to success, since the file type field (File_x0020_Type) for the document library is not updated when overwriting files using the Add method with the parameter overwrite = true.

Since there are no two documents with the same name in the same Sharepoint document library (and it does not make sense to require unique names from our users;)), Guid is generated as the name of the new document. After adding the name is replaced (“for beauty”) by “issued” when creating the element identifier (ID) plus the file name received from the client. The result is approximately the following: "3-Filename.docx". When a file is added, information about it is stored in our field (it is in the original TestList list, to which we added an SPFileField column).
SPFileFieldControl
  public class SPFileFieldControl : BaseFieldControl { // ,       private const string fileNotExist = "  "; //      private const string editTemplateName = "SPFileFieldControlEdit"; //      private const string displayTemplateName = "SPFileFieldControlDisplay"; //  ,    public string libraryName; //   Value,     FileValue private FileValue currentFile = null; public override object Value { get { return currentFile; } set { currentFile = (FileValue)value; } } public SPFileFieldControl(string aLibraryName) { libraryName = aLibraryName; } //     protected override void OnInit(EventArgs e) { currentFile = (FileValue)this.ItemFieldValue; base.OnInit(e); } //  ,      protected override string DefaultTemplateName { get { return base.ControlMode == SPControlMode.Display ? displayTemplateName : editTemplateName; } } public override string DisplayTemplateName { get { return displayTemplateName; } } //           protected override void CreateChildControls() { base.CreateChildControls(); if (base.ControlMode == SPControlMode.Display) { SetupDisplayTemplateControls(); } else { SetupEditTemplateControls(); } } //         private void SetupEditTemplateControls() { FileUpload fuDocument = (FileUpload)TemplateContainer.FindControl("fuDocument"); HyperLink aFile = (HyperLink)TemplateContainer.FindControl("aFile"); HiddenField hdFileName = (HiddenField)TemplateContainer.FindControl("hdFileName"); HtmlInputImage btnDelete = (HtmlInputImage)TemplateContainer.FindControl("btnDelete"); HtmlInputImage btnAdd = (HtmlInputImage)TemplateContainer.FindControl("btnAdd"); //          if (currentFile != null) { //      SPWeb web = SPContext.Current.Site.RootWeb; SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName)); SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID); //     aFile.NavigateUrl = SPUrlUtility.CombineUrl(web.ServerRelativeUrl, spFileItem.Url); aFile.Text = currentFile.Name; //      UniqueId  hdFileName.Value = spFileItem.UniqueId.ToString(); } else { //    ,     aFile.Text = fileNotExist; hdFileName.Value = String.Empty; } btnDelete.Attributes.Add("onclick", String.Format(@"clearFileValue('{0}','{1}','{2}','{3}');return false;", aFile.ClientID, fuDocument.ClientID, hdFileName.ClientID, fileNotExist)); btnAdd.Attributes.Add("onclick", String.Format(@"changeDisplay('{0}');return false;", fuDocument.ClientID)); fuDocument.Attributes.Add("onchange",String.Format(@"changeFileName(this,'{0}');return false;", aFile.ClientID)); } //         private void SetupDisplayTemplateControls() { if (currentFile != null) { //      SPWeb web = SPContext.Current.Site.RootWeb; SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName)); SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID); //     HyperLink aFile = (HyperLink)TemplateContainer.FindControl("aFile"); aFile.NavigateUrl = SPUrlUtility.CombineUrl(web.ServerRelativeUrl, spFileItem.Url); aFile.Text = currentFile.Name; } } //      public override void UpdateFieldValueInItem() { //    Page.Validate(); if (Page.IsValid) { // ,     FileUpload fuDocument = (FileUpload)TemplateContainer.FindControl("fuDocument"); HiddenField hdFileName = (HiddenField)TemplateContainer.FindControl("hdFileName"); //       , ,    if (hdFileName.Value == String.Empty && currentFile != null) { //   deleteFile(); //    currentFile = null; } //        if (fuDocument.HasFile) { SPWeb web = SPContext.Current.Site.RootWeb; SPFolder folder = web.GetFolder(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName)); //   ,   if (currentFile != null) { deleteFile(); } //        currentFile = addFile(folder, fuDocument); } base.UpdateFieldValueInItem(); } } //   private void deleteFile() { //       UniqueID    SPWeb web = SPContext.Current.Site.RootWeb; SPList list = web.GetList(SPUrlUtility.CombineUrl(web.ServerRelativeUrl, libraryName)); SPListItem spFileItem = list.GetItemByUniqueId(currentFile.UniqueID); spFileItem.Delete(); } //   private FileValue addFile(SPFolder folder, FileUpload fuDocument) { string uniqueName = Guid.NewGuid().ToString(); //     uniqueName SPFile spfile = folder.Files.Add(uniqueName, fuDocument.FileContent,true); //       spfile.Item["BaseName"] = String.Format("{1}-{0}", fuDocument.FileName, spfile.Item.ID); spfile.Item.Update(); return new FileValue() { Url = spfile.Item.Url, UniqueID = spfile.Item.UniqueId, Name = fuDocument.FileName }; } } 

ASCX Template Description


The resulting solution allows you to successfully create new type of site columns. However, forms are not shown yet, as Sharepoint cannot detect the rendering templates specified in the SPFileFieldControl class (RenderingTemplate). They are described in the ascx file in the CONTROLTEMPLATES folder. To create the corresponding file, add the User Control to the project and delete the automatically generated files with the ascx.cs and ascx.designer.cs extensions. The file describes two templates: a template for the SPFileFieldControlEdit editing form , consisting of a hyperlink to a file, fields for downloading a new file and add / delete buttons, and a template for the display form SPFileFieldControlDisplay , which contains one hyperlink to a file.
SPFileFieldControl.ascx
  <%--     --%> <SharePoint:RenderingTemplate ID="SPFileFieldControlEdit" runat="server"> <Template> <%--  CSS  JS --%> <SharePoint:CssRegistration ID="CssRegistration1" name="/_layouts/styles/FileFieldControl.css" after="corev4.css" runat="server"/> <SharePoint:ScriptLink ID="ScriptLink1" runat="server" Name="FileFieldControl.js" Localizable="false"/> <%--    --%> <asp:HyperLink ID="aFile" runat="server" CssClass="ffc-hl" EnableViewState="False"/> <%--   -     ) --%> <input type="image" ID="btnDelete" runat="server" src="/_layouts/images/DELETE.gif" alt="delete"/> <%--   -  FileUpload --%> <input type="image" ID="btnAdd" runat="server" src="/_layouts/images/newrowheader.png" alt="add"/> <br/> <asp:FileUpload ID="fuDocument" runat="server" CssClass="ffc-fu" /> <%--     UniqueId       --%> <asp:HiddenField runat="server" ID="hdFileName"/> </Template> </SharePoint:RenderingTemplate> <%--     --%> <SharePoint:RenderingTemplate ID="SPFileFieldControlDisplay" runat="server"> <Template> <%--  CSS --%> <SharePoint:CssRegistration ID="CssRegistration1" name="/_layouts/styles/FileFieldControl.css" after="corev4.css" runat="server"/> <%--    --%> <asp:HyperLink ID="aFile" runat="server" CssClass="ffc-hl" EnableViewState="False"/> </Template> </SharePoint:RenderingTemplate> 

Some JavaScript and CSS


In order for the solution to start working, you need to add a JavaScript file to the project (I added it to the LAYOUTS folder) and describe three functions in it: changeDisplay - show / hide an item by ID, clearFileValue - clear the link to the file, the hidden field and the file upload field, and changeFileName to display the name of the file being uploaded.
FileFieldControl.js
 // /    function changeDisplay(id) { var v = document.getElementById(id); if (v.style.display == 'block') //|| v.style.display == '') { v.style.display = 'none'; } else { v.style.display = 'block'; } } //    ,       function clearFileValue(aID, fID, hfId, defText) { var a = document.getElementById(aID); a.innerText = defText; a.removeAttribute("href"); var hf = document.getElementById(hfId); hf.value = ''; var f = document.getElementById(fID); f.outerHTML = f.outerHTML; } //    function changeFileName(oFile, aID) { var a = document.getElementById(aID); var fullPath = oFile.value; if (fullPath) { //           var startIndex = (fullPath.indexOf('\\') >= 0 ? fullPath.lastIndexOf('\\') : fullPath.lastIndexOf('/')); var filename = fullPath.substring(startIndex); if (filename.indexOf('\\') === 0 || filename.indexOf('/') === 0) { filename = filename.substring(1); } a.innerText = filename; } } 

Well, where without CSS! Add the FileFieldControl.css file to the STYLES folder, in which we specify the alignment for the hyperlink and that the FileUploader will be hidden by default.
FileFieldControl.css
 .ffc-hl { text-align:center; vertical-align:top; } .ffc-fu { margin-top: 3px; display: none; } 

Check!


Stop. First, it is worth mentioning the features). I found only one - for the list in which the columns of the new type are added, attachments should be prohibited . Otherwise, the standard forms generated by Sharepoint try to process the FileUpload control and the error "The list item was saved, but one or more attachments could not be saved."

Now check! Let me remind you that we created the TestList list, added a new type column named TestFile to it , and also added the TestLibrary document library , into which the files should ultimately fall. After all the work done, the form for creating the elements TestList looks like this:

When adding a file, our control takes the form:

And this is what the newly created elements look like in the TestList list:

And in the TestLibrary library:

SPFileField field on the view form:

On the edit form:

Notice the little cracks in the list item view? Correctly, by default, the value of FileValue described by us is displayed (file name, path to it, and UniqueID through the separator). To fix this, you need to turn to XSL.

Xsl


As you may have guessed, the XSL file must be added to the folder with the corresponding name. Be careful - Visual Studio suggests adding an XSLT file after searching for the 'XSL' template. It is necessary to replace the file extension with the one we need. It is also worth noting that the file name must begin with 'fldtypes_' . Read more about it here . You can also find the standard headers that have been omitted below. The XSL file itself describes the parsing of the string that contains the name and path to the file, and the output of the resulting hyperlink.
fldtypes_SPFileField.xsl
  <!--        SPFileField--> <xsl:template match="FieldRef[@FieldType='SPFileField']" mode="Text_body"> <!--    --> <xsl:param name="thisNode" select="."/> <xsl:variable name="full" select="$thisNode/@*[name()=current()/@Name]" /> <!--    ,      (  UniqueID,    ) --> <!-- ""      --> <xsl:variable name="name" select="substring-before(substring-after($full,';#'),';#')" /> <xsl:variable name="url" select="substring-before(substring-after(substring-after($full,';#'),';#'),';#')" /> <!--      --> <xsl:element name="a"> <xsl:attribute name="href"> <xsl:value-of select="concat($RootSiteUrl,'/',$url)"/> </xsl:attribute> <xsl:value-of select="$name"/> </xsl:element> </xsl:template> 

After deploying a solution with a new XSL file, the TestList list view will look more decent:


EventRecievers


The resulting solution does not provide for deleting files from TestLibrary when deleting elements of the TestList list. That is, if you delete the newly created elements ('untitled'), then the file 'Microsoft Word.docx document' will remain in the TestLibrary library. This behavior can be corrected with the help of EventReceivers (the code triggered upon the occurrence of a specific event). To do this, add an EventReceiver to the project. I called it ER_OnItemDeleting. We are interested in events for the elements of the list, namely the moment of deletion of the element (An item is being deleted), since after deletion we will lose the values ​​of all the fields, and therefore the file ID.

I did not have the necessary list for which it is necessary to handle events, so I chose any and then made corrections manually. To do this, in the Elements.xml file (inside ER_OnItemDeleting) for the Receivers tag, you must replace the ListTemplateId attribute with a ListUrl with the value Lists / TestList. You can bind an EventReceiver to a specific list using both ListTemplateId and ListUrl. The second is easier to learn, so I used it. The following EventReciever description appeared:
Elements.xml
 <?xml version="1.0" encoding="utf-8"?> <Elements xmlns="http://schemas.microsoft.com/sharepoint/"> <Receivers ListUrl="Lists/TestList"> <Receiver> <Name>ER_OnItemDeletingItemDeleting</Name> <Type>ItemDeleting</Type> <Assembly>$SharePoint.Project.AssemblyFullName$</Assembly> <Class>SPFileFieldControl.ER_OnItemDeleting.ER_OnItemDeleting</Class> <SequenceNumber>10000</SequenceNumber> </Receiver> </Receivers> </Elements> 

Custom triggers are considered Sharepoint for individual features. Therefore, after adding an EventReceiver, Feature1 will also be in the project. To make it easier to navigate later, I re-named the 'Triggers' feature, and in the Title field I added 'SPFileFieldControl Triggers'. The EventReceiver code itself is not complicated and lies in the fact that we iterate over the fields of the element being deleted in an attempt to find a column of type SPFileField. If the field is found and the file has been loaded, it is deleted.
ER_OnItemDeleting.cs
  public class ER_OnItemDeleting : SPItemEventReceiver { public const string ER_OnItemDeletingName = "SPFileFieldControl.ER_OnItemDeleting.ER_OnItemDeleting"; public const string SPFileFieldName = "SPFileField"; public override void ItemDeleting(SPItemEventProperties properties) { //        for (int i = 0; i < properties.ListItem.Fields.Count; i++) { //    SPFileField if (properties.ListItem.Fields[i].TypeAsString == ER_OnItemDeleting.SPFileFieldName) { //     FileValue fileValue = properties.ListItem[properties.ListItem.Fields[i].StaticName] as FileValue; if (fileValue == null) continue; //   SPFile spFile = properties.Web.GetFile(fileValue.UniqueID); spFile.Delete(); } } base.ItemDeleting(properties); } } 

By publishing a solution, you can make sure that the file from the TestLibrary library is really deleted when necessary (the feature should be activated automatically when the solution is deployed). But do not manually connect EventReceivers when adding an SPFileField type column to the list! It turns out that the event of creating a new column can also be traced and processed using EventReceiver. By analogy with the previous one, we will create a new EventReceiver that keeps track of List Events, such as adding and removing columns (a field was added / a field is being removed). Here is the final hierarchy of files in a project:

When developing EventReceivers, it is useful to look at the list of all available ones and correct them with pens if something is wrong . Below is the code that executes when adding and deleting columns of type SPFileField. When adding a column, we connect the above-described EventReceiver to the target list, which deletes the associated files when deleting the list items. And when deleting, we delete the associated files and, if there are no more SPFileField columns in the list, cancel processing of the OnItemDeleting event.
ER_OnFileFieldCRUD.cs
  public class ER_OnFileFieldCRUD : SPListEventReceiver { public const string SPFileFieldName = "SPFileField"; public override void FieldAdded(SPListEventProperties properties) { //    SPFileField,     ,     if (properties.Field.TypeDisplayName == SPFileFieldName) { //   EventReceiver,    bool eventReceiverExists = false; for (int i = 0; i < properties.List.EventReceivers.Count;i++) { if (properties.List.EventReceivers[i].Class == ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName) { eventReceiverExists = true; break; } } if (!eventReceiverExists) { properties.List.EventReceivers.Add(SPEventReceiverType.ItemDeleting, Assembly.GetExecutingAssembly().FullName, ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName); } } base.FieldAdded(properties); } public override void FieldDeleting(SPListEventProperties properties) { if (properties.Field.TypeDisplayName == SPFileFieldName) { //    SPFileField,  // 1)    ,    for (int i = 0; i < properties.List.Items.Count; i++) { SPListItem splistItem = properties.List.Items[i]; FileValue fileValue = splistItem[properties.Field.StaticName] as FileValue; if (fileValue == null) continue; SPFile spFile = properties.Web.GetFile(fileValue.UniqueID); spFile.Delete(); } // ,       SPFileField bool anyFileFieldExist = false; for (int i = 0; i < properties.List.Fields.Count; i++) { if (properties.List.Fields[i].TypeAsString == SPFileFieldName && properties.List.Fields[i].StaticName != properties.Field.StaticName) { anyFileFieldExist = true; break; } } // 2)   ,      SPFileField if (!anyFileFieldExist) { for (int i = 0; i < properties.List.EventReceivers.Count; ) { if (properties.List.EventReceivers[i].Class == ER_OnItemDeleting.ER_OnItemDeleting.ER_OnItemDeletingName) { properties.List.EventReceivers[i].Delete(); } else { i++; } } } } base.FieldDeleting(properties); } } 

At this point, I completed the development of the SPFileField column, although, as they say, “there is no limit to perfection”. For example, you can handle deleting a file from the document library directly (I don’t show this library to users) and clear the link to it (FileValue value). Or add a check for the existence of a document library when creating SPFileField columns, etc. In order not to limit you in your desires, I post the source code, as well as a ready-made solution .

Conclusion


The resulting solution allows you to create Sharepoint 2010 columns and “upload” files into them. In this case, the downloaded files are not standard attachments, which, after downloading, differ only in name. Files entered into the system through the SPFileField column have a link to a specific column that can be used to create your own View. Thus, the presented approach allows sharing file downloads and displaying information about them at the column level.
An additional advantage is that we are not limited to one SPFileField type column per list, and the SPFileField columns can be configured to upload files to different document libraries (it may be interesting to show the user and the associated document library).

At this I have everything, thank you for your attention!
I would like to say a special thank you to this resource , which describes in some detail the creation of Custom Field Types for Sharepoint 2007. As my experience shows, most of the writing is valid for Sharepoint 2010 as well.

Source: https://habr.com/ru/post/235493/


All Articles