Brad's profileSharePoint BlogPhotosBlogLists Tools Help

Blog


    March 25

    Renaming Title back to Title…

    I have just done this for a second time at a client’s site. Got a list, using content types, want to have the Title field appear in the list as something different (in my case, Question). So I happily rename the field, forgetting that this is a Site collection based field type that is in EVERY list – Damn, I hate racing the clock.

    Anyway, you can’t easily rename it back through the UI because “Title” is a reserved keyword (I reckon it’s a bug too) but you can using SharePoint Manager. SharePoint Manager is a great tool that lets you manipulate your SharePoint system in ways one could only dream about thru the User Interface – including bypassing that annoying “Reserved Keyword” message.

    Crack it open on one of your WFE servers, point to the right site, drill down until you get to the “Fields” collection. Underneath there, you will see the name you renamed title to – double check in the pane on the right and ensure the “Static Name” still says title. Then just edit the “Title” property back to Title and you’re done…

    Of course, the answer is to hide the field – but how? Mike Ferrara has the answer here. Essentially, you enable management of content types, set the field to “Hidden” and you’re on your way. Done! The only problem then is fixing up all of the other lists that have a “Title field. Let’s see, that’s… hmmm, carry the 2… All of them. Oh man…

    March 08

    Fishing an ItemID from a URL – the easy way

    I’ve recently started to follow Jeremy Jameson’s blog – He knows lots about MOSS and the associated technologies, and he always seems to be coming up with little “gems” of information. The post that led me to his blog was one he wrote where you can create a bookmark or quicklink which extracts the ItemID from the current URL in a browser.

    It’s a neat trick and could easily be expanded to a little toolbox of functionality for people who are constantly pulling things out of querystrings (like me). From his blog:


    Parse List Item ID
    How do I install the browser button?

    For Internet Explorer users:

    1. Right click the Parse List Item ID link and choose Add to Favorites.
    2. A security alert dialog will warn you that the link may be unsafe. Click Yes.
    3. In the Save in dropdown list, choose Links. Click Add.

    For Firefox users:

    1. Drag the Parse List Item ID link to your Links/Bookmarks toolbar.
    How do I uninstall the browser button?

    For Internet Explorer users:

    1. Right click the Parse List Item ID button on the Favorites toolbar and choose Delete.
    2. A confirmation dialog will warn you that the button will be permanently deleted. Click Yes.

    For Firefox users:

    1. Right click the Parse List Item ID button on the Links/Bookmarks toolbar and choose Delete.
    2. A confirmation dialog will warn you that the button will be permanently deleted. Click Yes.

    Cheers!

    Found a way to "Address" a SharePoint field using JavaScript

    One of the things that makes it a bit tough working with SharePoint pages is the fact that you cannot predict what the form fields will be called. They get random names like ctl00$m$g_b38c402f_5254_4ae1_8677_39beff22ef48$ctl00$ctl02$ctl00$ctl01$ctl00$ctl00$ctl02$ctl00$ctl00$ctl04$ctl00$ctl00$TextField and they change every time you change the form fields. Bah!

    Hopeless.

    However... in my hunt around for a solution to the Outlook Calendar times in GMT issue for one client, I came across this little gem of a script that solves this problem: http://blogs.msdn.com/sharepointdesigner/archive/2007/06/13/using-javascript-to-manipulate-a-list-form-field.aspx

    The getTagFromIdentifierAndTitle code below grabs a Form field tag (id) based on the tag Name, Identifier and Title you pass in. In this code sample, they are using that information to pre-populate form fields in SharePoint from values in the querystring. One of the projects we’re working on at the moment has a common requirement to have the form fields pre-populated - this may be useful in that solution somewhere.


    <script type="text/javascript">   
    // This javascript sets the default value of a lookup field identified
    // by <<FIELD DISPLAY NAME>> to the value stored in the querysting variable
    // identified by <<QUERYSTRING VARIABLE NAME>>     
    // Customize this javascript by replacing <<FIELD DISPLAY NAME>> and
    // <<QUERYSTRING VARIABLE NAME>> with appropriate values.
    // Then just paste it into NewForm.aspx inside PlaceHolderMain   
    _spBodyOnLoadFunctionNames.push("fillDefaultValues");   
    function fillDefaultValues() {
       var qs = location.search.substring(1, location.search.length);
       var args = qs.split("&");
       var vals = new Object();
       for (var i=0; i < args.length; i++) {
         var nameVal = args[i].split("=");
         var temp = unescape(nameVal[1]).split('+');
         nameVal[1] = temp.join(' ');
         vals[nameVal[0]] = nameVal[1];
       }
        setLookupFromFieldName("<<FIELD DISPLAY NAME>>", vals["<<QUERYSTRING VARIABLE NAME>>"]); }

    function setLookupFromFieldName(fieldName, value) {
       if (value == undefined) return;
       var theSelect = getTagFromIdentifierAndTitle("select","Lookup",fieldName);
      // if theSelect is null, it means that the target list has more than
      // 20 items, and the Lookup is being rendered with an input element
        if (theSelect == null) {
           var theInput = getTagFromIdentifierAndTitle("input","",fieldName);
           ShowDropdown(theInput.id);
          //this function is provided by SharePoint
           var opt=document.getElementById(theInput.opt);
           setSelectedOption(opt, value);
           OptLoseFocus(opt);
          //this function is provided by SharePoint  
        } else {
         setSelectedOption(theSelect, value);
       }
    }  

    function setSelectedOption(select, value) {
       var opts = select.options;   var l = opts.length;
       if (select == null) return;   for (var i=0; i < l; i++) {
         if (opts[i].value == value) {
           select.selectedIndex = i;
           return true;
         }
       }
       return false;
    }

    function getTagFromIdentifierAndTitle(tagName, identifier, title) {
       var len = identifier.length;
       var tags = document.getElementsByTagName(tagName);
       for (var i=0; i < tags.length; i++) {
         var tempString = tags[i].id;
         if (tags[i].title == title && (identifier == "" || tempString.indexOf(identifier) == tempString.length - len)) {
           return tags[i];
         }
       }
       return null;
    }
    </script>


    Here's a list of the most common SharePoint Field types and their associated Identifier and tagName:

    SharePoint Field Type identifier tagName
    Single Line of Text TextField input
    Multiple Lines of Text TextField input
    Number TextField input
    Currency TextField input
    Choice (dropdown) DropDownChoice select
    Lookup (single)* Lookup select
    Lookup (multiple) SelectCandidate; SelectResult select
    Yes/No BooleanField input

    *Lookups are a bit more complicated because Lookup FormFields render differently when the target list contains more than 20 items. That's why the code above has a special function for Lookups

    March 07

    Hamlet, Act 2 scene 2 (Oh how I hate Daylight Savings, TimeZones, and the fact XML can’t deliver a date format JavaScript understands)

     Yes, while working onsite at a Client’s place I've been getting my hands all icky with code... I scrub and I scrub but all of the ocean's waters won't wash this code from my hands.

    Anyway, the point of this blog entry is to record the challenge that I had and the code I wrote to fix it, as it will be useful in the future for both me and maybe others. Essentially, the page I was fixing was giving me times in Zulu (GMT) - and in an XML format the JavaScript time functions could not understand. The business requirement was to have the times appear in Outlook in the same time zone the user was using. It took me the better part of a day, and I learned way too much about time zones and how to detect them on the user's computer, but I got there in the end.

    The original code for this was here - http://patrikluca.blogspot.com/2008/03/extension-to-rooms-and-equipment.html - an "Add to Outlook" button to go beside every Resource reservation, for a company still using a Lotus Notes app that allowed them to place multiple conflicting (overlapping) bookings on the same room / item.

    I leveraged the free "Resource Reservation" add-in for SharePoint and enhanced it according to the article, but when the users started using it they noticed that the times never matched. It was always 10 hours earlier than the real booking time that appeared in the page. Hmmm... 10 hours... Australia's GMT+10, so it must be passing them in as GMT Times.

    I updated the JavaScript so the times were changed back to the equivalent time zone (taking into account daylight savings) of the current user (Useful because this client is in 4 different time zones at any time as a minimum). There's only one problem in the code (and it's unlikely to be hit) but if a user books a resource for either side of a daylight savings change, then one of the times will be out by 1 hour. You can see the note I've put in there to remind myself if I ever get asked to fix it :)

    If you are copying the code, watch out for line breaks!


    function createOutlookAppointment(title,meetinglocation,startdate,enddate)    
    {
    // if you ever get asked to manage the booking hours over a DST change period, you will need to
    // test each date to see if it is in dst - isDST returns 1 if it is during DST, and 0 if not
    // if (isDST(startdate) - isDST(enddate)) = -1 then the end time is during DST
    // if (isDST(startdate) - isDST(enddate)) = 1 then the start time is during DST
    // however if the calculation = 0 then leave 'em be
    //   newAppt = new appt(title, meetinglocation, formatIncomingDateTime(startdate + '_' + isDST(startdate)), formatIncomingDateTime(enddate) + '_' + isDST(enddate));
       newAppt = new appt(title, meetinglocation, formatIncomingDateTime(startdate), formatIncomingDateTime(enddate));

      saveAppt( newAppt );
    }

    function isDST(incomingDateTime)
    {
    // This function detects if the inbound date and time is in Daylight savings time, and returns a 1 if it is, and a 0 if it's not
    // Kinda handy, seeing as DST is always "1" hour difference
    //I've dropped this in so I don't have to find it again if I ever do need to fix it
      var yearPart = incomingDateTime.substring(0,4);
      var monthPart = incomingDateTime.substring(5,7);
      var dayPart = incomingDateTime.substring(8,10);
      var d = new Date(yearPart, monthPart, dayPart);
    //   var d=new Date();
       var dY=d.getFullYear();
       var d1=new Date(dY,0,1,0,0,0,0);   
       var d2=new Date(dY,6,1,0,0,0,0);  
       var d1a=new Date((d1.toUTCString()).replace(" GMT",""));  
       var d2a=new Date((d2.toUTCString()).replace(" GMT",""));  
       var o1=(d1-d1a)/3600000;  
       var o2=(d2-d2a)/3600000;  
       var rV=0;  
       if (o1!=o2)
       {   
         d.setHours(0);
         d.setMinutes(0);
         d.setSeconds(0);
         d.setMilliseconds(0); 
         var da=new Date((d.toUTCString()).replace(" GMT",""));
         o3=(d-da)/3600000;   
         rV=(o3==o1)?0:1;  
        }
      return rV;
    }

    function formatIncomingDateTime(incomingDateTime){

      var timePartHours = incomingDateTime.substring(11,13);
      var timePartMinSecs = incomingDateTime.substring(13,16);
      var yearPart = incomingDateTime.substring(0,4);
      var monthPart = incomingDateTime.substring(5,7);
      var dayPart = incomingDateTime.substring(8,10);
      var newDate = new Date(yearPart, monthPart, dayPart); //gives us the current date and time in UTC
      var timeOffset = (newDate.getTimezoneOffset())/60; //gives us the amount of hours we need to deduct

      //create the date we are going to manipulate
      var myDate = new Date(yearPart, monthPart, dayPart);

      //calculate the hour we are booking for...
      timePartHoursInt = parseInt(timePartHours,10) - timeOffset;

      if (timePartHoursInt < 0) {
       //This means the booking hours + the time offset are really for yesterday...
       //because the timeoffset is the difference between current local time and GMT
       //so a negative number means we need to deduct a day and change it into real time again by adding 24h
       timePartHoursInt += 24;
       myDate.setDate(myDate.getDate() - 1);
      }
      else if (timePartHoursInt >= 24) {
       //in this case, we are booking for tomorrow (and midnight - 00:00:00 - is tomorrow)
       timePartHoursInt -= 24;
       myDate.setDate(myDate.getDate() + 1);
      }

    if (timePartHoursInt < 10) {
    // add a leading 0 to the time string so it's formatted in a standard way
    // then convert the int to a string
      timePartHours = '0'+timePartHoursInt.toString();
    }
    else {
      timePartHours = timePartHoursInt.toString();
    }

      if (myDate.getMonth() < 10)
    // add a leading 0 to the time string so it's formatted in a standard way
    // then convert the int to a string
       monthPart = '0'+myDate.getMonth();
      else
        monthPart = myDate.getMonth();

      if (myDate.getDate() < 10)
    // add a leading 0 to the time string so it's formatted in a standard way
    // then convert the int to a string
        dayPart = '0'+myDate.getDate();
      else
        dayPart = myDate.getDate();

      var datePart = myDate.getYear() + '-' + monthPart + '-' + dayPart;

      return datePart.concat(' '.concat(timePartHours).concat(timePartMinSecs));
    }

    function appt( Subject, Location, Start, End){

      this.Subject = Subject;
      this.Location = Location;
      this.Start = Start;
      this.End = End;

      this.ReminderMinutesBeforeStart = 15;
    }
    function saveAppt( obj ){

      var olAppointmentItem = 1;

      out = new ActiveXObject( "Outlook.Application" );

      appt = out.CreateItem( olAppointmentItem );

      appt.Subject = obj.Subject;
      appt.Location = obj.Location;
      appt.Start = obj.Start;
      appt.End = obj.End;

      appt.ReminderMinutesBeforeStart = obj.ReminderMinutesBeforeStart;         

      appt.Display();
      return null;
    }      


    This "Add to Outlook" button can be used on any page that can pass in a Start and End date, time, a Title (for the calendar appointment) and a location (eg a calendaring Confluence site?). With a bit of modification you could also add other attendees, a reminder, etc.

    However, the site hosting the link / button must be trusted for this to work properly. If the site appears in the "Internet Zone" you just get a JavaScript error. If your site sits in the Intranet security zone you get a security dialogue, asking you if you are sure you want to run this crazy activeX control.

    Anyway, an interesting day and a successful outcome for the client.Although a word of warning – the Room and Resource Reservation solution from Microsoft will deploy successfully on a Publishing website, however it will NOT stop users from booking an overlapping meeting (because there’s a conflicting named column or content type – one or the other).

    Brad

    March 04

    All my life, I’ve wanted a consistent Menu system…

    Up until now, menu systems for MOSS had to be built if you created new site collections that resided below the “parent” site (Parent in a URL-hierarchal sense). The team at RDA have released a few projects, but by far and away the best one from them (IMHO) is the Global Navigation solution in CodePlex.

    Basically, you can nominate a web site or set of web sites you want to have the navigation based on, then using a separate administration page, Staple it to whichever web sites you want. I hope to try it out on one of our lab servers soon, and if it works as described I’ll just start rolling this out as a matter of course for all of the clients I create intranet sites for, where you will often have separate site collections with separate navigation in the same URL hierarchy.

    You can get the code and binaries for it here – there’s also a blog entry with screen shots here – I just wish they’d explain what they mean by the phrase “associating a Data Source with all sites” – Is the web site the data source? A Database table?

    Cheers!
    Brad