Tuesday, November 15, 2016

SharePoint 2013 - Redirect to View Item Form on New Item Save

Scenario
  • You are saving a new record and want the user to be redirected back to the view item form to review the item.  This will allow the user to start a workflow or perform another action on the ribbon without the need to find the item in the list or library view.
  • Need the solution to use JSOM (client side script) only.  
  • Should work in Office 365, SharePoint Online, and On-Prem.
Issue
  • By default, SharePoint redirects you to the List View you most recently came from.
  • There is no out of the box setting for this.
Resolution
  • We solve this by adding a redirect script to each list view page that does the following:
    • Check if a new item was added by the current user.
    • Check and create a short term cookie to determine if they have already been redirected for this item
    • Redirect the user if they haven't already been
  • First add the following Javascript file to your SiteAssests/js folder.  
    • Create the "js" folder if it's not already there.
    • Replace the ALLCAPS hard coded values with appropriate values.

SiteAssets/js/SP.RedirectOnAddItem.js

var siteUrl = '/sites/SITENAME';

function createCookie(name,value,minutes) {
    if (minutes) {
        var date = new Date();
        date.setTime(date.getTime()+(minutes*60*1000));
        var expires = "; expires="+date.toGMTString();
    }
    else var expires = "";
    document.cookie = name+"="+value+expires+"; path=/";
}

function readCookie(name) {
    var nameEQ = name + "=";
    var ca = document.cookie.split(';');
    for(var i=0;i < ca.length;i++) {
        var c = ca[i];
        while (c.charAt(0)==' ') c = c.substring(1,c.length);
        if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
    }
    return null;
}

function eraseCookie(name) {
    createCookie(name,"",-1);
}

function RedirectOnAddItem() {
var clientContext = new SP.ClientContext(siteUrl);
var web = clientContext.get_web();
this.currentUser = web.get_currentUser();
clientContext.load(currentUser);
clientContext.executeQueryAsync(
Function.createDelegate(this, onUserQuerySucceeded),
Function.createDelegate(this, onQueryFailed)
);   
}

function onUserQuerySucceeded(sender, args) {
    var email = this.currentUser.get_email();
    var userid = this.currentUser.get_id();
    var loginName = this.currentUser.get_loginName();
    var clientContext = new SP.ClientContext(siteUrl);
var oList = clientContext.get_web().get_lists().getByTitle('LISTORLIBRARYNAME');       
    var camlQuery = new SP.CamlQuery();
    camlQuery.set_viewXml('<View><Query><OrderBy><FieldRef Name = "ID" Ascending = "FALSE"/></OrderBy><Where><Eq><FieldRef Name="Author" LookupId="True"/><Value Type="User">' + userid  + '</Value></Eq></Where></Query><RowLimit>1</RowLimit></View>');
    this.collListItem = oList.getItems(camlQuery);
       
    clientContext.load(this.collListItem);
       
    clientContext.executeQueryAsync(Function.createDelegate(this, this.onListQuerySucceeded), Function.createDelegate(this, this.onQueryFailed));  
}

function onListQuerySucceeded(sender, args) {
var listItemInfo = '';
var listItemEnumerator = collListItem.getEnumerator();
       
    while (listItemEnumerator.moveNext()) {
        var oListItem = listItemEnumerator.get_current();
        listItemInfo += '\nID: ' + oListItem.get_id();
        listItemInfo += '\nAuthor: ' + oListItem.get_item("Author").get_lookupValue();
        var diff = Math.abs(Date.now()) - new Date(oListItem.get_item("Created"));
        var minutesPassed = Math.floor((diff/1000)/60);
        listItemInfo += '\nCreated (Minutes Ago): ' + minutesPassed ;
       
        if (oListItem && minutesPassed < 2) {
//alert(listItemInfo.toString());
if (!(readCookie('PANnewID') == oListItem.get_id().toString())){
createCookie('PANnewID', oListItem.get_id().toString(), 2);
window.location = "/sites/SITENAME/Lists/LISTORLIBRARYNAME/DISPLAYFORMNAME.aspx?ID=" + oListItem.get_id();
}
}
    }
}

function onQueryFailed(sender, args) {
alert('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
}
  • Finally, add the following code to a Script Editor web part on each List View page where the user would normally be redirected.
<script src="/sites/SITENAME/SiteAssets/js/SP.RedirectOnAddItem.js"></script>
<script type="text/javascript">ExecuteOrDelayUntilScriptLoaded(RedirectOnAddItem, "sp.js");</script>

Tuesday, July 26, 2016

Is SharePoint a true Document Management System (DMS)? Rumor explored.

In my 10+ years working with SharePoint I've heard 3 individuals declare that SharePoint isn't "a true Document Management system".  When questioned about this they bring up instances of poorly architected solution issues or performance limitations as the justification.  I too have experienced poor performance, bugs, limitations, and have inherited poorly architected SharePoint solutions; but my tendency is to address the underlying issues or engineer proper solutions rather than discounting the software out-of-hand.  This leads me to believe that there are some outdated primary sources for this rumor that were introduced outside of these users' own experiences.  This article will look into some of these sources, listing their bias.  I will also post some refuting articles.

Disclaimer: Obviously my bias is towards SharePoint as I am very familiar with its capabilities and have used it to implement more than 40 document management solutions with each one exceeding its unique requirements

Sources of Rumor (SharePoint is not a DMS)
  • eFileCabinet - SharePoint competitor (2010)
  • ContentVerse - SharePoint competitor (2014)
  • Fishbowl Solutions - Oracle WebCenter SharePoint Connector provider (2012?)
    • Has a good overview of the history of SharePoint issues pre-2010 but is mostly propaganda for keeping WebCenter and using their connector instead of migrating everything to SharePoint
  • Reva Solutions - Alfresco (SharePoint Competitor) ISV (2015)
  • DocFinity - SharePoint competitor
  • Lexmark's In Context - Perceptive Software (SharePoint Competitor - now Lexmark) interview  (2011)
Refutations of Rumor (SharePoint is a DMS)
Conclusion

SharePoint (2007 or later) is a bonafide Document Management System with its own unique advantages and disadvantages.  The rumors appear to have been initiated by competing vendors as marketing propaganda from sources with limited knowledge of SharePoint's capabilities or from early reviewers who balked at the new technology and its use of third-party vendors for imaging and other advanced functionality.  It's fair to mention that SharePoint 2007 (pre-2010) was missing some of the more enterprise scale features of a DMS, but even those features were not mandatory to consider that version a "true DMS" as typically defined unless third-party products were excluded from use.

The closest you can get to the original rumor while maintaining the truth is that "SharePoint is not just a true Document Management System." Even the terms "Document Management System" and "File Management System (FMS)" are outdated.  "Enterprise Content Management System" (ECM, ECMS, or CMS) is now the preferred moniker to describe platforms that do more than just manage files and documents.  SharePoint, being one of the most widely used ECMs, benefits from the fact that it also takes on collaboration, intranets, extranets, (WCM) web content management, workflow, insights, enterprise search, and more.  It does so while maintaining one of the largest ISV (partner) communities of any ECMS, including most of the other ECM vendors who are struggling to maintain their relevance by integrating with SharePoint and Office 365.

This brings about the final argument against SharePoint, "it's not specialized only for document management, thus taking on too much and spreading too thin."  I agree that if the entire SharePoint team focused on just the DMS side, then it would be more feature rich in that area.  However, the true benefit of SharePoint over other DMSs is that it is a multi-tool that excels in many areas, each with fringe benefits to document management.  This is one of the primary reasons for the mass migration from single-focus systems to broader platforms.

Note: The meaning of "true DMS" is subjective, therefore if you define a "true DMS" to include a specific limitation (ex: Must be able to render historical versions in search without exposing the versioned documents in a library or folder - SharePoint Limitation), then you can justify your claim.  Just realize that anyone else can do the same to your preferred system (ex: Must provide secure co-authoring capabilities in a web-based note-taking client on MS, iOS, and Android mobile devices - available only with SharePoint).

Feedback?

Please contribute comments below listing specific features that your favorite CMS has which SharePoint may not.  I will do my best to provide feedback on its support within SharePoint.

Tuesday, July 19, 2016

Restricted Edit Event Receiver - SharePoint 2013

Scenario
  • You need to restrict edits to a document library based on the current value of a field.
  • Full trust farm solution for SharePoint 2013 scoped to Web.
Solution
  • I created a configurable event receiver that will restrict edits (metadata and content) based on the value in one of the document's metadata columns.
  • Additional feature: Delay implementation of the restriction for a number of seconds from Created Date to allow for additional automated processes to update new documents.
  • Additional feature: Exclude a document from the restriction based on a regular expression using its  filename.
  • Additional feature: Automatically populate the Title field (or other text field) based on another choice or text field.
  • Open-Source code is available here:  RestrictedSave ZIP
    • Feel free to modify and use this code.  You may not resell it or a modified version of it as part of a packaged solution without my permission.
  • Disclaimer:  This will only prevent edits that trigger the item updating event receiver.  Some programmatic and 3rd party API calls made using bulk edit operations, workflow actions, or other event receivers may bypass the event receiver.  In addition, this code is not hardened for high security scenarios and is only meant as a first line defense against unwanted typical user edits.
Installation
  • Download the RestrictedSave WSP file and deploy to your farm.
  • Enable the Restricted Save feature on your sub-site.
    • Feature Description:
      Configured via the RestrictedSave list. If this site does not have one, a RestrictedSave list will be added. Document libraries named in the RestrictedSave list will not save a file if the user is unauthorized and the assigned column contains a certain value. If no required permission is specified, then the field can only be edited if the restricted column's value is changed. Multiple document libraries, columns, or values may be configured but must exist in this subsite. Optional New File Delay and Regular Expression Exclusion may be configured as well. Will also set the document title and/or filename based on a choice field by using the 'Title Choice Field' columns.
  • Configure the RestrictedSave list that was added to the site according to your needs.
Example

  • A standard configuration of the RestrictedSave list to restrict edits on a document library named "RestrictDeleteTest" when the "LOBType" column is set to "Choice 1".

  • The resultant error displayed on a document's edit properties form in the "RestrictDeleteTest" library when a filename modification is attempted.


  • Error Message in clear text
UNABLE TO SAVE! -- When column "LOBType" is set to "Choice 1" the item is locked and cannot be updated. -- Please work from a new copy or change the column value.





RestrictedSave List Configurable Fields
  • Instructions are provided in-line on the form.
  • Add one line for each restriction.  All lines will be parsed for each document edited in the sub-site.

--------------------------------------------------------------------------------------


And again in text format for search-engines, visually impaired, and copy/paste:

Document Library Name *


Column Name


Column Value


Required Permission To Edit

   

New Item Delay Seconds


Title Exclusion Regex


Title Choice Field Name


Title Choice Target

   

Thursday, July 7, 2016

Hacking the Datasheet View / Quick Edit in SharePoint 2013 to display in an IFrame

Scenario
  • I created a document library list view in datasheet (quick edit) mode for display in a custom popup dialog in a SharePoint 2013 Visual Studio project "application page".
    • This grid was used to make last minute changes to metadata values of selected items before continuing an existing process in a larger solution.
  • I needed to display the list view using an IFrame pointing to a URL filtering the values by IDs.
  • This was a large list exceeding the list view threshold
  • I needed to hide all chrome, navigation, ribbon, and other elements surrounding the list view web part.
  • I needed to call a JavaScript function outside the list view page from a button on the list view page.
  • Note that I couldn't figure out how to convert the application page into a web part page which is why the list view web part is included via an IFrame rather than embedded directly into the page, however the same issues would apply except without the need for a hidden button or the chrome trimming.
  • An alternative to using the list view web part in datasheet mode would be to use a datagrid control and manage the CRUD programatically, but this would be harder to maintain if the SharePoint schema changed down the line and would require much more coding to implement correctly.
Issues
  • In Datasheet view the last record edited (or possibly the only record) will only save if you select another record (which you can't even do if there's only one record). This was an issue for 2 reasons:
    • There was a custom button in the app that when clicked would continue the process without saving the record that was just edited.
    • The stop editing functionality which would normally be used to save the last record resulted in a list view threshold error dialog due to a bug in the datagrid / Quick Edit mode when in a folder or using filter parameters and using the "Stop editing this list" link because it forwards to a standard view without the filter or folder.
Resolution
  • Implementing the IFrame: A bug in Visual Studio .Net 4.5 framework IFrame web control requires the control to be instantiated manually in the code behind rather than automatically in the designer code file
protected global::System.Web.UI.HtmlControls.HtmlGenericControl iframe1;
  • The IFrame was added to the application page like so:
 <iframe name="iframe1" id="iframe1" ClientIDMode="Static" runat="server" height="400" width="400" seamless" />
  • The IFrame was instantiated dynamically using server side code that set the filter parameters in the query string like so (itemids are in a semicolon delimited format):
iframe1.Attributes["Src"] = "/sites/SiteName/LibraryName/Forms/DatasheetViewName.aspx?IsDlg=1&FilterName=ID&FilterMultiValue=" + itemids;
  • I hid a button (id = btnCopy style:display = none) on the application page which ran the application code and added the following JavaScript function to the application page so that it could be called from the list view page from within the IFrame.  Note the timed delay here as I'll explain this later.
<script type="text/javascript">
    function triggerBtnCopyClick() {
        setTimeout(function () { document.getElementById('btnCopy').click(); }, 1200);   
    }
    
    ....  existing code used by the application page that handled the btnCopy click event.
</script>
  • I added a script editor web part to the Datasheet List View page and configured it to remove the chrome elements still remaining after IsDlg=1 did its work.  Note that your class names may be different based on the WPQ number (find using your browser's developer tools).
    <style>
    #s4-ribbonrow{display: none;}
    #Hero-WPQ2{display: none;}
    #CSRListViewControlDivWPQ2{display:none;}
    </style>
    • Finally, I added the button and code to call the parent page's triggerBtnCopyClick code.  You will need to find the GUID of your list view web part (webpartid) and plug it into the code.
      This is the part that involved hacking the datasheet view by finding the function calls the "Stop editing this list" button used to save the final changes and reusing them in my own code.  The downside to this is that the grid needs to be refreshed afterwards and takes a second to finish processing so I added a 1200ms delay into the triggerBtnCopyClick function above that processes the code after the save.
    <input type="button" id="SaveEdits" value="Save and Process" onclick="var gridInitInfo = g_SPGridInitInfo[('{WebPartID GUID HERE}')]; var ganttControl = window[gridInitInfo.controllerId]; var ganttControl = window[gridInitInfo.controllerId]; ganttControl.TryDispose(function(dlgReturnValue) {window.location.reload(false);}); this.style.display = 'none'; window.parent.triggerBtnCopyClick(); return false" />



    Wednesday, June 1, 2016

    SharePoint End Date on Workday using a Holiday List

    Scenario
    • Typical scenario where you need to add a certain number of days to a start date and set the due date to a working day (Ex: service window).  Note that this is not "number of working days".  It is "the next working day after X days".
    • You can easily adapt this code to provide the last working day before X days.
    • The working day must bypass weekends and holidays.
    • You have a Holidays list in SharePoint that you can query.
      • The Holidays list uses the out of the box "Holiday" list content type.
        • Note: In code, the "Holiday" "Date" field's internal name is "V4HolidayDate"
    • You need to add some code to an event receiver or custom workflow action to get this date.
    Solution
    • This was coded for SharePoint 2013 but should work for other versions

           protected void MAIN_FUNCTION()  
           {  
             DateTime startdate = Convert.ToDateTime(START_DATE_VALUE).Date;  
             DateTime finaldate = startdate.AddDays(Convert.ToInt16(FINAL_DATE_VALUE));  
             SPList holidayLibrary = web.GetList("/Lists/Holidays");  
             Int16 CurrentHolidayIndex = 0;  
      
             SPQuery query = new SPQuery();  
             query.Query = string.Concat(  
                     "<Where><And><Geq>",  
                      "<FieldRef Name='V4HolidayDate'/>",  
                      "<Value Type='DateTime'>", SPUtility.CreateISO8601DateTimeFromSystemDateTime(finaldate) , "</Value>",  
                     "</Geq><Leq>",  
                      "<FieldRef Name='V4HolidayDate'/>",  
                      "<Value Type='DateTime'>", SPUtility.CreateISO8601DateTimeFromSystemDateTime(finaldate.AddDays(14)), "</Value>",  
                     "</Leq></And></Where>",  
                     "<OrderBy>",  
                      "<FieldRef Name='Date' Ascending='FALSE' />",  
                     "</OrderBy>");  
             query.ViewFields = "<FieldRef Name='V4HolidayDate' />";  
             query.ViewFieldsOnly = true;  
             //Limited to 14 days of continuous holidays  
        
             while (IsWeekend(finaldate) || IsHoliday(finaldate, holidays, ref CurrentHolidayIndex))  
             {  
               finaldate = finaldate.AddDays(1);  
             }  
             OUTPUT_FIELD_VALUE = finaldate.ToShortDateString();  
           }  
      
           bool IsWeekend(DateTime date)  
           {  
             if (date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday) return true;  
             return false;  
           }  
      
           bool IsHoliday(DateTime date, SPListItemCollection holidays, ref Int16 holidayIndex)  
           {  
             for (; holidayIndex < holidays.Count; holidayIndex++)  
             {  
               DateTime holiday = Convert.ToDateTime(holidays[holidayIndex]["V4HolidayDate"]);  
               if (date == holiday) return true;  
               if (holiday > date) break; //stop checking once we pass the current date  
             }  
             return false;  
           }