List Items in SharePoint Meeting Workspaces

Sunday, April 22, 2012

3

Lists in SharePoint Meeting Workspaces with multiple instances (a recurring meeting, or multiple single meetings) can be in one of two modes:

  • List items are shared across all meeting instances
  • List items are specific to a meeting instance

Most of the OOB lists in a Meeting Workspace are by default in the 2nd mode.  If you add a Task item to one meeting instance, it will not show up when you navigate to another instance.  This gives the user experience of each meeting instance having its own list, but there's really just one list that's "instance-aware".


Making a List Shared

To make a list shared from the UI, go to List Settings --> Advanced Settings, and select Yes for Share List Items Across All Meetings:


To do the same programmatically, set SPList.MultipleDataList to FALSE.

Retrieving Instance-specific List Items

To retrieve items from a specific meeting instance, set the SPQuery.MeetingInstanceId property when querying the list.  For recurring meetings, the InstanceID is an integer that represents the meeting date in yyyyMMdd format (ie. 20120422 for April 22nd, 2012).  In workspaces with multiple non-recurring meetings, the InstanceID is just an identifier like 1, 2, 3, etc..

The example below queries for all Tasks with the title "My Task" under the 20120422 meeting instance:

SPList taskList = SPContext.Current.Web.Lists["Tasks"];

SPQuery query = new SPQuery();

query.MeetingInstanceId = 20120422;
query.Query = "<Where><Eq><FieldRef Name=\"Title\" /><Value Type=\"Text\">My Task</Value></Eq></Where>";

SPListItemCollection results = taskList.GetItems(query);

To retrieve items across all instances from a non-shared list, set SPQuery.MeetingInstanceId to the SPMeeting.SpecialInstance.AllButSeries enumeration.  You have to cast the enum to an int.

Adding Instance-specific List Items

To add a list item to a specific meeting instance, simply set the item's InstanceID.  Property name is case sensitive, and value must be of type int, not string:

SPList taskList = SPContext.Current.Web.Lists["Tasks"];

SPListItem newItem = taskList.Items.Add();
newItem["Title"] = "My Task";
newItem["InstanceID"] = 20120422;

newItem.Update(); 

For more tips such as how to find current or future recurring meeting instance IDs, check out my Working with SharePoint Recurring Meeting Workspaces post.


Deploying Multi-select Managed Metadata Fields

Saturday, April 21, 2012

10

Taxonomy site columns can be provisioned just like other columns through a Feature. There are already excellent tutorials out there on how to deploy site columns via Feature--for instance, this post on Fabian William's blog.  However, there are some pitfalls to getting the Elements file right for Managed Metadata fields (MM for short hereafter).  This post covers:

  • How to provision a MM field
  • TargetTemplate property's function and it's effect on TaxonomyFieldControl
  • How to enable multi-select

Hidden Note Field 

Firstly, each MM field must be paired with a hidden Note field.  The MM field stores the value of the metadata term, while the associated Note field holds the term's taxonomy ID.  If you provision just the MM one, it'll show up on the site, but you'll probably run into this error once you try to put values in: Failed to get value of the “{0}” column from the “Managed Metadata” field type control. See details in log. Exception message: Invalid field name. {00000000-0000-0000-0000-000000000000}.

The example below deploys a new MM column called Keywords, and a Note column called KeywordsTaxHTField0.  The Note field can be named anything, but I use the TaxHTField0 suffix to be consistent with SharePoint's convention.

The MM field's TextField property references the Note field's ID. This is what links the two columns.

<Field ID="{AFFCC398-1B80-49B4-9367-5980C74AF556}"
            Type="Note"
            Name="KeywordsTaxHTField0"
            StaticName="KeywordsTaxHTField0"
            DisplayName="Keywords_0"
            ShowInViewForms="false"
            Hidden="true"
            CanToggleHidden="true"
            Group="Custom" />

<Field ID="{9D21CCB4-B815-483E-A1C1-9947A1514187}"
            Type="TaxonomyFieldType"
            Name="Keywords"
            StaticName="Keywords"
            DisplayName="Keywords"
            ShowField="Term1033"
            Required="false"
            Group="Custom">
   <Customization>
      <ArrayOfProperty>
         <Property>
            <Name>TextField</Name>
            <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
                         p4:type="q6:string"
                         xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
               AFFCC398-1B80-49B4-9367-5980C74AF556
            </Value>
          </Property>
      </ArrayOfProperty>
   </Customization>
</Field>


TargetTemplate and TaxonomyFieldControl Rendering

Sometimes, in my twisted dreams, SharePoint comes to me, a goddess with eyes afire, whose alien desires and inscrutable whims lay waste to entire farms.  She passes judgement atop a giant hourglass, a literal timer job run on the raging souls of a thousand broken developers.  A simpler metaphor might be a box of chocolates.  The point is, some crazy, existential sh*t happens on SharePoint.

Say you add a MM field to an Enterprise Wiki and put down a TaxonomyFieldControl on the page to display the new field, right under the OOB Wiki Categories control with this markup:

<Taxonomy:TaxonomyFieldControl
    FieldName="Wiki_x0020_Page_x0020_Categories"
    EmptyValueDescriptionForTargetTemplate="<%$Resources:cms,enterwiki_nocategories_assigned%>"
    DisableInputFieldLabel="true"
    runat="server" />

<Taxonomy:TaxonomyFieldControl
    FieldName="Keywords"
    EmptyValueDescriptionForTargetTemplate="No keywords tagged"
    DisableInputFieldLabel="true"
    runat="server" />

The declarations are identical except for the fieldname and message to display when the field is empty.  The two TaxonomyFieldControls render completely differently:


OOB wiki categories display line by line, each linked to a page listing all articles tagged with that category.  The second taxonomy control is a comma-separated text list.  It also refuses to display the EmptyValueDescription when no tags are selected.  Scritch... scritch...


Much blood, tears and other fluids were unwillingly shed--unspeakable rituals performed--in search of an answer.  It turns out, to get the TaxonomyFieldControl to render like Wiki Categories, the Managed Metadata field it points to must have it's TargetTemplate property set.  TargetTemplate specifies the URL for the term hyperlinks.  For instance, TargetTemplate for Wiki Categories is /_layouts/Categories.aspx, a page which lists all articles tagged with the category.  You can point to that, or make a better looking custom page.  To set TargetTemplate, include the following property in your MM field declaration:

<Property>
     <Name>TargetTemplate</Name>
     <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
                  p4:type="q6:string"
                  xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
          /_layouts/Categories.aspx
     </Value>
</Property>


Allow Multiple Values 

To make a multi-select MM field, declare the field Type as TaxonomyFieldTypeMulti instead of TaxonomyFieldType, and set Mult to true.  You must also explicitly set Sortable to false; otherwise multi-select will not be enabled despite the Type and Mult properties!

The full example below provisions a multi-select MM field with TargetTemplate, along with the associated hidden Note field:

<Field ID="{AFFCC398-1B80-49B4-9367-5980C74AF556}"
           Type="Note"
           Name="KeywordsTaxHTField0"
           StaticName="KeywordsTaxHTField0"
           DisplayName="Keywords_0"
           ShowInViewForms="false"
           Hidden="true"
           CanToggleHidden="true"
           Group="Custom" />

<Field ID="{9D21CCB4-B815-483E-A1C1-9947A1514187}"
           Type="TaxonomyFieldTypeMulti"
           Name="Keywords"
           StaticName="Keywords"
           DisplayName="Keywords"
           ShowField="Term1033"
           Required="FALSE"
           Mult="TRUE"
           Sortable="FALSE"
           Group="Custom">
     <Customization>
          <ArrayOfProperty>
               <Property>
                   <Name>TextField</Name>
                    <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
                                 p4:type="q6:string"
                                 xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
                         AFFCC398-1B80-49B4-9367-5980C74AF556
                    </Value>
               </Property>
              <Property>
                  <Name>TargetTemplate</Name>
                  <Value xmlns:q6="http://www.w3.org/2001/XMLSchema"
                               p4:type="q6:string"
                               xmlns:p4="http://www.w3.org/2001/XMLSchema-instance">
                      /_layouts/Categories.aspx
                  </Value>
              </Property>
          </ArrayOfProperty>
     </Customization>
</Field>


Working with SharePoint Recurring Meeting Workspaces

Tuesday, April 17, 2012

1

There seems to be sadly scant documentation out there on working with Meeting Workspaces through the SharePoint API.  Especially when it comes to recurring meetings, some (seemingly) basic pieces of meeting information can be very tricky to retrieve.  Here're a few that I've dealt with on a recent project:

  • What meeting instance is the user currently browsing?
  • Is the current workspace for a recurring meeting?
  • Given a specific date, when is the next recurrence?
  • What are the recurrences for the next X months?

For tips on working with list items in recurring Meeting Workspaces, check out my List Items in SharePoint Recurring Meeting Workspaces post.


What meeting instance is the user currently browsing?

You can get this by creating a SPMeeting object from SPContext.Current:

if (SPContext.Current != null)
    && SPContext.Current.Web != null
    && SPMeeting.IsMeetingWorkspaceWeb(SPContext.Current.Web))
{
  SPMeeting meeting = SPMeeting.GetMeetingInformation(SPContext.Current.Web);
  return meeting.InstanceId;
}


Is the current meeting workspace for a recurring meeting?

You can get this by creating a SPMeeting object from SPContext.Current and seeing if MeetingCount is -1:

if (SPContext.Current != null
    && SPContext.Current.Web != null
    && SPMeeting.IsMeetingWorkspaceWeb(SPContext.Current.Web))
{
  SPMeeting meeting = SPMeeting.GetMeetingInformation(SPContext.Current.Web);
  bool isRecurring = (meeting.MeetingCount == -1);
 
  return isRecurring;
}

What is the next meeting recurrence?  Or the recurrences for the next 6 months?

This one is quite tricky.  My initial thought was to query the hidden Meeting Series list, which keeps track of all the meeting instances in a workspace.

Unfortunately, an instance is only added to the Meeting Series list when a user browses to that instance for the first time.  If you create a new meeting workspace with 10 recurrences, Meeting Series will initially contain a single row, representing the entire meeting series.  When someone browses to say, the 3rd recurrence, that will then be added to Meeting Series.  And so forth.  This actually makes sense, because recurring meetings can be indefinite and the list can't be populated with infinite recurrences!

Poking around the OOB webparts and dlls, I found that SharePoint itself uses this obscure piece of CAML to retrieve future instances, ordered by start date:

<Where>
     <DateRangesOverlap>
         <FieldRef Name="EventDate" />
         <FieldRef Name="EndDate" />
         <FieldRef Name="RecurrenceID" />
         <Value Type="DateTime">
             <Month />
         </Value>
     </DateRangesOverlap>
</Where>
<OrderBy>
     <FieldRef Name="EventDate" />
</OrderBy>

This, combined with a couple key SPQuery properties will return all meeting instances within a specified month, regardless of whether they've been visited or not.  More often than not this query will return more items than the ItemCount for Meeting Series!

SPList list = SPContext.Current.Web.Lists["Meeting Series"];
 
SPQuery query = new SPQuery();
query.CalendarDate = new DateTime(2014, 04, 16);
query.Query = "";   // CAML query above
query.ExpandRecurrence = true;
query.MeetingInstanceId = (int)SPMeeting.SpecialInstance.AllButSeries;   
query.RowLimit = 50;
 
SPListItemCollection results = list.GetItems(query); 

The code above returns all meeting instances in the month specified by CalendarDate (April 2014).  The particular day doesn't matter.  The results may include the last week of March and first week of May.  ExpandRecurrence must be set to TRUE to include unvisited meeting instances.

Aside from <Month /> under <DateRangesOverlap>, the CAML query can also specify: <Now />, <Today />, <Week />, and <Year />.  Unfortunately, <Year /> seems to return completely non-sensical results.  I've tried it on multiple workspaces, and it never returns instances within the specified year like you'd expect.

So.  Back to the original questions.  Since <Year /> doesn't work, to find all instances within the next 6 months, I query one month at a time, incrementing Calendar date with each iteration.  Similarly, to find the next recurrence, I search forward one month at a time until an instance is found, like so:

public static int GetNextMeetingInstanceId(int currentInstanceId)
{
   int nextInstanceId = currentInstanceId;
 
   SPList list = SPContext.Current.Web.Lists["Meeting Series"];
 
   // Convert instanceID to DateTime
   DateTime searchDate = DateTime.ParseExact(currentInstanceId.ToString(), 
                           "yyyyMMdd", CultureInfo.InvariantCulture);
 
   // Search up to the next 12 months for the next instance
   for (int i = 0; i <= 12; i++)
   {
      DateTime monthToSearch= searchDate.AddMonths(i);
 
      // Retrieve instances for the month we're currently searching through, 
      // ordered by start date
 
      SPQuery query = new SPQuery();
      query.CalendarDate = monthToSearch;
      query.Query = ""; // CAML query above
      query.ExpandRecurrence = true;
      query.MeetingInstanceId = (int)SPMeeting.SpecialInstance.AllButSeries; 
      query.RowLimit = 50;
 
      SPListItemCollection results = list.GetItems(query);
 
      foreach (SPListItem instance in results)
      {
         int instanceId = (int)instance["InstanceID"];
         if (instanceId > currentInstanceId)
         {
            nextInstanceId = instanceId;
            return nextInstanceId;
         }
      }
   }
 
   return nextInstanceId;
}