Friday, January 17, 2014

Hide Specific Buttons on a SharePoint 2010 Ribbon Using Javascript

I recently had a project where I needed to have the user take action on SharePoint data using a custom form, and then have a workflow handle the updates into another library, thus preventing the user from directly managing the main body of data. Contribute permissions were required because SharePoint Workflows don't have the ability to 'run as' admin or service or any other user or to set permissions on an item in the workflow. Both of those would be great additions to the SharePoint Workflow foundation.

So I used a combination of custom forms, specific permission levels, audience targeted web parts and other means to show the user only what they need to see and create "security through obscurity."

Contribute(edit) permissions are required in this case, but I wanted to allow a user to "view" a list item without having the prominent "Manage" ribbon group and the "Edit" button at the top of the form page (dispform.aspx, etc...)

There are fairly easy ways to hide the ribbon from certain users which work well but it's not granular enough and my permissions on this site are not that simple. It is also possible to create a custom ribbon and/or code and deploy to achieve these things.

BUT:
I wanted an easy, quick, and non-invasive means of shutting off certain buttons. So here is what I did.

The ribbon is split up into tabs. Each tab can have one or more groups of controls, and each group can hold one or more controls.




It's easy to find the id of any one of these and then use javascript to edit the object.

In Internet Explorer, hit F12 (developer tools) or use Firebug or some other equivalent in another browser.
Find the ribbon using the click tool or search for "<div class="loaded" id="RibbonContainer">"


You can see the structure of the whole ribbon there along with all of the element IDs. Pay attention to the CSS classes for an easy way to pick out the pieces.



OK, so anyone familiar with javascript knows that you can grab the object and set a style parameter like this:
object.style.parameter = value;
So it is logical that to hide the group, I just set object.style.display = 'none'; and it will disappear.

Not too complex, except that SharePoint ribbons are loaded when needed, not on page load, so you can't just put some javascript at the end of the page and hide the element. So first you need to make sure the object exists. If it does not exist, wait and try it again in some period of time. And lastly, if you don't want a looping script, so set a counter to make sure it only tries a few times before completely giving up.

Here is my example using the "Clipboard" group

<script language="javascript">
var cntr = 0; //set counter
function hidegroup(){
    if(document.getElementById('Ribbon.EditingTools.CPEditTab.Clipboard') != null){ //check to make sure object exists
        document.getElementById('Ribbon.EditingTools.CPEditTab.Clipboard').style.display = 'none'; //hide it
    }else if(cntr<6){//does not exist, check counter
        cntr++;//increment counter
        setTimeout('hidegroup()', 1000); //wait 1 sec and then repeat
    }
}
</script>
  • The counter starts at zero and once it runs 5 times, it quits.
  • If the object exists, it hides it. I noticed that if you set innerHTML to an empty string, the other buttons may not function.
  • If the object doesn't exist it increases the counter by one and then waits 1,000 miliseconds and then calls the function again.
To use this on a page, edit and place a content editor web part on the page under the form web part, edit the HTML of the content editor web part and put the script into the HTML field.



Set the web part to hidden (Edit Web Part -> layout (section) -> Hidden) so that nobody sees it. Hidden web parts are still on the page and the javascript will run just fine.

The last step is to use the web part's audience targeting field (in the "Advanced" section) to decide who will see the web part. The targeting can be done on groups and/or users. Whoever is targeted will have the web part, and whoever has the web part will have the ribbon group disabled.



This is perfect for a form like dispform.aspx or editform.aspx that automatically displays the ribbon. The only delay is in loading the ribbon. Other pages have ribbons that load contextually whenever the user interacts with a control that has a ribbon. Removing controls or groups for those pages could be done by removing the counter, which would cause a constant loop waiting for the element to show up.



For me, this works perfectly because it was a quick fix, can be targeted to a SharePoint security group, can remove specific items, and is all done in the browser.

Disclaimer: this is by no means the best way, but it is very simple and quick.


Sunday, August 25, 2013

URL Encode in JS URL decode in PHP

Recently I had a task where I needed to find a Javascript function that would mimic the PHP urlencode function.

The first thing I tried was encodeURIComponent(). This turns spaces into %20, single-quotes into %27 or something like that. So I did a search and found these out-of-the-box options for URL encoding in Javascript:

  • escape() will not encode: @*/+
  • encodeURI() will not encode: ~!@#$&*()=:/,;?+'
  • encodeURIComponent() will not encode: ~!*()'
Encoded test string: "Jimmy's Coffee & Bean Emporium":

In Javascript using encodeURIComponent()
Jimmy's%20Coffee%20%26%20Bean%20Emporium
In Javascript using escape()
Jimmy%27s%20Coffee%20%26%20Bean%20Emporiu
In Javascript using encodeURI()
Jimmy's%20Coffee%20&%20Bean%20Emporium
In PHP using urlencode()
Jimmy%27s+Coffee+%26+Bean+Emporium

None of the encoded strings are the same.

The solution:

I created a new Javascript function called php_urlencode() to encode the same way PHP does.

function php_urlencode(str) {
    return escape(str).replace(/\+/g,'%2B').replace(/%20/g, '+').replace(/\*/g, '%2A').replace(/\//g, '%2F').replace(/@/g, '%40');
}
This function uses the escape function first and then uses the "replace()" regular expression function to replace a few strings with something PHP would use instead. It uses the "/g" global replace property to replace all instances of the string instead of just the first one.

  • + becomes %2B (done first to encode valid "plus" symbols)
  • %20 becomes + (done after the first to turn spaces into the "plus" symbol)
  • * becomes %2A
  • / becomes %2F
  • @ becomes %40

This function returns:
Jimmy%27s+Coffee+%26+Bean+Emporium
In PHP using urlencode()
Jimmy%27s+Coffee+%26+Bean+Emporium

It matches! Very nice.

Thursday, June 27, 2013

VBA: Set cursor to end of MS Access field

This came about because I have a form that periodically saves the form record using the OnTimer event. When a save is initiated, the Microsoft Access default behavior is to place the cursor at the front of the current field.

This default behavior would interrupt the user if they are typing, and it would require them to scroll down if the text is much larger than the scroll box. Either way, it is an annoyance.

 Access has an option to change the default behavior and these settings apply to all of the databases opened by that users' instance of Access.

  •  File - Options - Client Settings - "Behavior Entering Field" (Access 2010)
  •  Office Button - Access Options - Advanced - "Behavior Entering Field" (Access 2007) 

The options are:
  • Select Entire Field 
  • Go to Start of Field 
  • Go to End of Field
Changing the defaults for everything may be a solution for you, but it is not for me. I simply want to change certain fields on certain forms so that when a user is typing and the OnTimer event is triggered, their cursor is not pushed back to the top of the field causing them to enter text in the wrong place.

There is one thing do to to duplicate the behavior for an individual field. Set the "SelStart" property to the end of the field. I do this in the OnEnter event so that clicking, tabbing, or other means of entering the field will automatically place the cursor at the end of the field value. Of course you have to do this for each field that needs it, but the benefit is that it allows you to pick and choose which fields it applies to.

Private Sub  txtFieldName_Enter()
'SelStart = end of the field. Add the ("" &) to ensure it works with a null field value.
me.txtFieldName.SelStart = Len("" & me.txtFieldName)
End Sub 

This solves the default behavior so that the need to scroll to end is removed. The second part of the fix is to ensure that when saving the form record, the cursor is still in current field and appears at the end.

This requires a global variable within the module to keep track of which field is current.
Dim strCurrField as String

And the Timer event:

Private Sub Form_Timer()
If Me.Dirty then
DoCmd.RunCommand acCmdSaveRecord 'save
Me.Controls(strCurrField).SetFocus ' cannot use the SelStart property without focus
Me.Controls(strCurrField).SelStart = Len("" & Me.Controls(strCurrField).Value)
End if
End Sub

So now when the form record is saved while a user is typing, the field blinks and the cursor continues at the end, which is the most likely place it belongs.

This does not help if the user is typing in the middle of a block of text, but in my instance, this is not a common scenario.

Tuesday, April 2, 2013

DSNless Access Table with Primary Key Index

I was recently working on a project where I used MS Access 2007 to build a multi-user application with a SQL backend. The nature of the application required the linked SQL tables to be DSNless.

I attach my DSNless tables in code, which offers a few benefits. By having the objects to attach listed in an a local Access table, I can attach and delete the linked tables on demand without the user knowing, and I can easily port this application to a new SQL database without much work. More useful is the ability to ensure that each time the application is opened, the table schemas are refreshed and changes made to SQL tables and views are reflected in Access.

My list of attachable objects looks like this:
SourceObjectLocalObjectServerDatabaseKeyField
tableXYdbo_tableXYSVRNameDBName(null)
viewXYdbo_viewXYSVRNameDBNameviewkeyid

I use a function that I found online and have since adapted to do the extra stuff I needed.

Function AttachDSNLessTable(stLocalTableName As String, stRemoteTableName As String, stServer As String, stDatabase As String, Optional stUsername As String, Optional stPassword As String)
    On Error GoTo AttachDSNLessTable_Err
    Dim td As TableDef
    Dim idx As Index
    Dim stConnect As String
  
    For Each td In CurrentDb.TableDefs
        If td.Name = stLocalTableName Then
            CurrentDb.TableDefs.Delete stLocalTableName
        End If
    Next
    
    If Len(stUsername) = 0 Then
        '//Use trusted authentication if stUsername is not supplied.
        stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";Trusted_Connection=Yes"
    Else
        '//WARNING: This will save the username and the password with the linked table information.
        stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";UID=" & stUsername & ";PWD=" & stPassword
    End If
    Set td = CurrentDb.CreateTableDef(stLocalTableName, dbAttachSavePWD, stRemoteTableName, stConnect)
    CurrentDb.TableDefs.Append td
  
    AttachDSNLessTable = True
    Exit Function

AttachDSNLessTable_Err:
    AttachDSNLessTable = False
    MsgBox "AttachDSNLessTable encountered an unexpected error: " & Err.Description

End Function

The problem:
In Access when you link a table manually (with a DSN) you will be prompted to select the primary key field if Access cannot get the information from the source object. If there is a key field assigned in SQL, that information will carry over to Access. This is not the case for SQL views.

Why you need an index in Access:
You cannot update records in a linked SQL view/table without a primary key index setup in Access. Your Acess statusbar will say' Recordset not updateable.' This will be likely to happen if you are converting an Access application to a SQL backend.

Solution - creating an index in code:
Attaching SQL objects in code will not present a primary key selection prompt, so you'll need a way to create that index in code.

There are a lot of posts online about using the DAO object model to create and attach an index kind of like this:

Set tdf = currentDB.CreateTableDef("tablename") 'new table def
currentDB.TableDefs.Append tdf 'append to the db
Set idx = tdf.CreateIndex("PrimaryKey") 'new index
With idx
     .Fields.Append .CreateField("ID")
     .Primary = True
End With
tdf.Indexes.Append idx 'append index

This never worked for me. Tons of people swear that it works, but I could never get it to work. *Notice that the tableDef has to be created and attached to the currentDB before creating the index.

What works for me:
I added an optional variable to the function called "keyField" that brings in that value from my table, which is only populated for objects that do not already. have a primary key in SQL.

Add this to the function definition: Optional stKeyfield As String

Function AttachDSNLessTable(stLocalTableName As String, stRemoteTableName As String, stServer As String, stDatabase As String, Optional stUsername As String, Optional stPassword As String, Optional stKeyfield As String)

Access will automatically setup an index for objects with primary keys, so in my list of objects to attach (a table), I only assign a keyfield to items that are SQL views because trying to create an index on a table will report that an index already exists.

In the function I add this code after appending the tableDef to the currentDB:

If Len(stKeyfield) > 0 Then
        CurrentDb.Execute "CREATE UNIQUE INDEX 'PrimaryKey' ON " & stLocalTableName & " (" & stKeyfield & ") WITH PRIMARY"
    End If

This works fine for me. I get to pre-define which field will be my primary key and every time that object is attached, it also adds the index. The SQL view is updateable in SQL, and with that index it is also updateable in Access.

Final function:

Function AttachDSNLessTable(stLocalTableName As String, stRemoteTableName As String, stServer As String, stDatabase As String, Optional stUsername As String, Optional stPassword As String, Optional stKeyfield As String)
    On Error GoTo AttachDSNLessTable_Err
    Dim td As TableDef
    Dim idx As Index
    Dim stConnect As String
   
    For Each td In CurrentDb.TableDefs
        If td.Name = stLocalTableName Then
            CurrentDb.TableDefs.Delete stLocalTableName
        End If
    Next
     
    If Len(stUsername) = 0 Then
        '//Use trusted authentication if stUsername is not supplied.
        stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";Trusted_Connection=Yes"
    Else
        '//WARNING: This will save the username and the password with the linked table information.
        stConnect = "ODBC;DRIVER=SQL Server;SERVER=" & stServer & ";DATABASE=" & stDatabase & ";UID=" & stUsername & ";PWD=" & stPassword
    End If
    Set td = CurrentDb.CreateTableDef(stLocalTableName, dbAttachSavePWD, stRemoteTableName, stConnect)
    CurrentDb.TableDefs.Append td
   
    If Len(stKeyfield) > 0 Then
        CurrentDb.Execute "CREATE UNIQUE INDEX 'PrimaryKey' ON " & stLocalTableName & " (" & stKeyfield & ") WITH PRIMARY"
    End If

   
    AttachDSNLessTable = True
    Exit Function

AttachDSNLessTable_Err:
    AttachDSNLessTable = False
    MsgBox "AttachDSNLessTable encountered an unexpected error: " & Err.Description

End Function

Friday, February 15, 2013

CAML, IntApp, SharePoint


I just got done doing a bunch of work in IntApp Integration Builder where I had to do things like reading/updating SharePoint lists and libraries.

This post/blog is never going to be highly detailed in any one area. I have no interest in explaining CAML, XML, IntApp, web services, logic, etc. There are plenty of resources online to find out how to write a CAML query. The reason I write this stuff down at all is because I have spent a lot of time looking for solutions that have been solved by others... but they don't usually share the information. Lots of questions with few answers. The goal here is to share the answers that I've found so someone else doesn't have to go through the try/fail/try again steps to get the answer.


If you use IntApp, they have a lot of great resources. You'll need to sign up on their customer community portal, but you can then find templates for solutions. This is the barebones "here is how we did this" kind of template that lets you skip past the first 20% of the setup.

So here are the basics:

SharePoint Web Services:
Every MS SharePoint site has a set of web services.
You can access them via URL by adding "_vti_bin/lists.asmx" to the end of your site URL like this:
http://yoursite/_vti_bin/lists.asmx
You'll need to us WSDL format (web services description language) so you add "?wsdl" to the end of the URL.
http://yoursite/_vti_bin/lists.asmx?wsdl
"Lists.asmx" is one of many web services. Lists.asmx gives you access to the lists in your SharePoint site. A library is a sub-type of list, so from now on, I'll just call it a list.

On to the CAML:
(Collaborative Application Markup Language)


You'll need a SOAP (Simple Object Access Protocol) wrapper around your query.
<soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
     <soap12:Body>

          Your web service call goes here
     </soap12:Body>
</soap12:Envelope>
 You'll also need the web service call. Each web service has multiple functions. I'll use "GetListItems" as the example. The name should be self-explanatory... it "Get"s a "List"s "Items". At least one piece of information you need... "listName" is the GUID of  the list or library that you wish to query.

 <GetListItems xmlns="http://schemas.microsoft.com/sharepoint/soap/">
     <listName>_YOURLISTGUID_</listName>
     <viewName>_YOURVIEWGUID_</viewName>
     Your CAML query goes here (or somewhere in the "GetListItems" tag.) The order shouldn't matter)
     <ViewFields>
          <FieldRef Name="ID" />
          <FieldRef Name="Title" />
          <FieldRef Name="yourfield" />
          ...etc
     </ViewFields>
     <rowLimit>0</rowLimit>
</GetListItems>

Quirk #1:
When querying a SharePoint web service, the "ViewFields" don't matter. You need to have the tag there, and it can be empty "<ViewFields></ViewFields>", but don't bother listing the fields you want to have returned in the dataset. The fields that are returned are defined in the view.
The "viewName" GUID is optional. If you leave it out, the list's "default" view will be used and you will only be able to access the fields that are returned in that default view. So if you've added a custom column into your list and it's not in the default view, you will need to add it there or create another view containing the fields you need and then use "viewName" to tell the web service which view to return.

** How you find your SharePoint list or view GUID? **

OK, So new we just need a query. There a lot of good resources online to figure out how to write a CAML query.

Quirk #2:
Normally your CAML query is wrapped in this:
<Query>...</Query>
With these web services you'll need to put an extra <query> tag (lower-case "q") wrapping the whole thing, like this:
<query><Query>...</Query></query>
If you don't do this, your query will be ignored, the web service will be called, and your dataset will be all values from the view sorted in the default sort order for the view that you've chosen (or the default view if you've not set a viewName GUID). If you've got data returned and the filter or sort doesn't work, this is probably why.


By the way the "<rowLimit>" tag works just fine. Use "0" to return all, or another number to limit the dataset to that number of records.


Here are some MORE TIPS for working with IntApp Relationship Builder and SharePoint web services.

SharePoint Web Service Dataset Basics

More useful information:
The "GetListItems" function of the http://yoursite/_vti_bin/lists.asmx?wsdl SharePoint web service returns XML and you need to have this path to access the returned data:
soap:Envelope/soap:Body/GetListItemsResponse/GetListItemsResult/listitems/rs:data/z:row
The nodes involved (and therefore the path) will differ depending on which web service and function you are calling.

If you work with XML you'll understand nodes and how to decipher the path.
Logically, XML is simply nested data. the first node is soap:Envelope. Inside that is soap:Body, inside that is GetListItemsResponse, and so on.

<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
    <soap:Body>
        <GetListItemsResponse xmlns="http://schemas.microsoft.com/sharepoint/soap/">
            <GetListItemsResult>
                <listitems xmlns:s='uuid:yourGUID' xmlns:dt='uuid:yourGUID' xmlns:rs='urn:schemas-microsoft-com:rowset' xmlns:z='#RowsetSchema'>
                    <rs:data ItemCount="X">
                        <z:row ows_FieldName1='FieldValue' ows_FieldName2='FieldValue'/>
                        <z:row... etc>
                    <rs:data>
                <listitems>
            </GetListItemsResult>
        </GetListItemsResponse>
    </soap:Body>
</soap:Envelope>
Tips:
  • SharePoint prepends "ows_" onto the field names.  i.e. "Title" becomes "ows_Title".
  • Any space in the field name is replaced with "_x0020_". i.e. "My Field" becomes "My_x0020_Field".
  • Calculated fields return "string;#" before the value. i.e. a value of "Jimmy" becomes "string;#Jimmy" and you'll have to strip that or at least compensate for it before you use the data.

Here is a good post on SharePoint field naming conventions over at Web BorG. 

SharePoint GUIDs


A GUID is a Globally Unique Identifier.
Here is how you find your SharePoint list or view GUID:
The easiest way is to go to the list's default view (SP2010) and hover over the "Add Document"/Add New Item" link on the bottom of the page. In the browser status bar you'll see the URL for that link. That URL contains the bare GUID for the list.

http://yoursite/_layouts/Upload.aspx?List={EFG33460-E1F4-4E78-A2F8-CDEG5BE0420B}&RootFolder=

Your GUID is in curly brackets: {EFG33460-E1F4-4E78-A2F8-CDEG5BE0420B}

If you don't have that link to click on, you can go to the list settings, copy the URL and see the GUID in its HTML encoded form in the "List" querystring variable (List=listGUID):
http://yoursite/_layouts/listedit.aspx?List=%7BEFG33460%2DE1F4%2D4E78%2DA2F8%2DCDEG5BE0420B%7D
It takes 5 seconds to manually transform this to a valid GUID. %7B = "{", %7D = "}", and %2D = "-".

List=%7BEFG33460%2DE1F4%2D4E78%2DA2F8%2DCDEG5BE0420B%7D
becomes
List={EFG33460-E1F4-4E78-A2F8-CDEG5BE0420B}

You can get your "view" GUID this way:
Go to the list settings, scroll all the way down to the views, and get the URL from your view (right-click "copy shortcut" or just click on it and copy the page URL). The view GUID is stored in its HTML encoded form in the "View" querystring variable (View=viewGUID). You can then manually decode that using the same logic as described above.
http://yoursite/_layouts/ViewEdit.aspx?List=%7B1637E544%2D5183%2D417B%2DB9DC%2DCD0E2E4EDEE1%7D&View=%7B7E510143%2DD7EA%2D4CFD%2D9A4D%2D65FF5AE909CC%7D&Source=

So.... This
View=%7B7E510143%2DD7EA%2D4CFD%2D9A4D%2D65FF5AE909CC%7D
becomes
View={7E510143-D7EA-4CFD-9A4D-65FF5AE909CC}