Enum to Drop Down List Html Helper Extension for ASP MVC 3

Monday, December 24, 2012

0

In this post I'll go over how to build a MVC3 HtmlHelper extension to map Enum model properties to a dropdown listbox.

The extension will follow the same pattern as out-of-box generic helper functions like @Html.LabelFor<> and @Html.TextBoxFor<>.

Say we have a simple Employee model with Name (string) and Department (nullable enum) fields, and need to make a form for creating new employees:

public enum Department
{
Development,
Sales,
HR
}

public class Employee
{
public string Name { get; set; }
public Department? Dept { get; set; }
}

We can create a label and textbox for the Name with this View cshtml:

@model Employee

<h2>New Employee</h2>

@Html.LabelFor(model => model.Name)
<br />
@Html.TextBoxFor(model => model.Name)

Which renders into:


There is, however, no way to handle the Department field.  Our goal is to build a custom HtmlHelper to map enum fields like Department to a drop down list.  If the field is Nullable, the dropdown needs to include an empty option.  The end product will work like this:

@Html.LabelFor(model => model.Dept)
<br />
@Html.DropDownEnumListFor(model => model.Dept, Department.Sales)

and render into:

Create Extension Method

Luckily, we don't have to start from scratch.  MVC3 already has a DropDownListFor<> helper, which takes as input a SelectList object containing objects to populate the dropdown.

The bulk of our work will be in building an appropriate SelectList from a given enum.

First, let's create the DropDownEnumListFor<> extension method.  Add a static class to your MVC project.  It doesn't matter where you put it or what it's called.  Remember to import System.Web.Mvc, System.Web.Mvc.Html, and System.Linq.Expressions namespaces.

public static class Extensions
{
public static MvcHtmlString DropDownEnumListFor<TModel, TValue>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TValue>> expr,
TValue selectedItem)
{
return helper.DropDownListFor(expr, ToSelectList<TValue>(selectedItem));
}

public static SelectList ToSelectList<T>(T selectedValue)
{
throw new NotImplementedException();
}
}

The expr parameter is a lamba function that takes as input the model and returns your property value.  On the View page, this is the model => model.Dept part.  selectedItem is the enum value that should be selected initially.

At this point, the new method should be available in Intellisense.  Remember to add a using statement in the View cshtml for the namespace your extension lives under.


Now we have to implement ToSelectList<T>, which converts any given Enum type T to a SelectList.

Handle Nullable Enums

Let's start by creating two helper methods to deal with Nullable enum types.

public static bool IsTypeNullable(Type type)
{
return (type.IsGenericType
&& type.GetGenericTypeDefinition() == typeof(Nullable<>));
}

public static Type GetBaseType(Type type)
{
return IsTypeNullable(type) ? type.GetGenericArguments()[0] : type;
}

IsTypeNullable simply checks if the specified type is Nullable.  GetBaseType converts a Nullable type to it's non-nullable type (ie decimal? to decimal).

Implement ToSelectList

Finally, implement ToSelectList<T>:

  1. If T is Nullable, retrieve the Enum type with GetBaseType
  2. If T is Nullable, add an empty option
  3. Get list of enum values using Enum.GetValues
  4. Build an ArrayList pairing each enum value with its display name
  5. Create a SelectList from the ArrayList

Code:

public static SelectList ToSelectList<T>(T selectedValue)
{
bool isNullable = IsTypeNullable( typeof(T) );
Type enumType = GetBaseType( typeof(T) );

if (enumType.IsEnum)
{
ArrayList items = new ArrayList();

// Add empty option
if (isNullable)
{
items.Add( new { Name = "", Value = default(T) } );
}

// Add enum values
var enumValues= Enum.GetValues(enumType);

foreach (T value in enumValues)
{
string displayName = Enum.GetName(enumType, value);
items.Add( new { Name = displayName, Value = value } );
}

return new SelectList(items, "Value", "Name", selectedValue);
}

return null;
}

Done!


2 Miles

Thursday, December 13, 2012

0

Goal: be able to run 2 miles in 15 minutes

I've come to the terrible realization that I can no longer claim in-shapness without deep, crushing shame, and a long string of qualifiers.  Well... I could run 2 miles if I really wanted to... on a good day... when I'm warmed up and the beam of a full moon crosses the shadow ofWhat, now? Right now?  Yeah... today's not really a good day...

I've fallen to the lot of the fitness have-nots.  This actually happened long ago, but I consoled myself that there were probably folks worse off.  I was solidly upper-lower class.  The fact of the matter is, I can't run 2 miles if Natalie Portman in her glorious Amidala regalia was standing at the end.  Well, I'd die trying.

Hence, this 2 mile goal.  I shall spend half an hour before work every day at the gym, another gerbil trying to outrun that cruel treadmill.  There's a simple but effective strategy from the project management playbook: formalize a goal, break it into discrete, quantifiable chunks, and consistently measure progress, and you've gone a long way toward actually achieving that goal.  I will, therefore, record here my progress every couple days.  Mile by terrible mile.


Nov 2821:00
Aaaaaaaaaaaaaaa..........

Dec 03 — 20:30
Shins really hurting at the end... could barely walk rest of day

Dec 05 — 20:00
Made it on 6 mph the whole way

Dec 06 — 19:40
Sped up to 8 mph last .15 mile.  I think I'm starting to settle into the routine.  Muscles no longer hurt afterward.

Dec 07
Utter failure. Tried to go up to 6.5 mph.  Had to stop after 1.5 miles.  Made up a mile at 7 mph.

Dec 12 — 18:30
6.5 mph the whole way.  Gettin it done.  Muscles weren't that strained, but breathing was very difficult last half mile.

Dec 13 — 18:00
Went up to 7.7 mph the last quarter mile.  It's getting harder and harder to speed up.  Cardio, not muscle fatigue, is the limiting factor.  Couldn't wake up today and had to run after work.

Gym tip: There's much debate on whether morning or afternoon workouts are better.  A lot of folks don't take environmental factors into account.  For instance, if you must use the gym showers, early morning wins hands down.  Afternoons mean sharing stalls with at least 3 other large hairless apes who, ironically, have far too much hair.  You don't want that.  Workout 6:30 AM, and make other people deal with your pilary leavings.

Dec 14 — 17:40
Did last half mile on 7.8 mph.  HALF WAY THERE!!  The easy half.  But whatever, this is still cause for rejoice.

Dec 18 — 17:15
Squeezed out 6.8, with last half mile on 7.5 mph.  Unfortunately, the gym will be closed for a week over the holidays :(

Dec 26 —
I AM DISAPPOINTS.  First day back at it after the holidays.  Could not finish last quarter mile.

Dec 27 — 17:00
6.8 mph, last half mile on 8.  Milestone breached.  With blood and iron.

Jan 25 — 16:40
First day back on the treadmill today!  I'd stopped running for about a month due to gym closures and illness.  Made half a mile on 8mph and the rest on 7, which is better than before the break.  I was quite impressed with myself.  Then I read that the current marathon record is about 12 mph.  For TWENTY SIX miles.  I think I prefer ignorance.

Feb 7 — 16:30
I have a sneaking suspicion my 16:40 time on the 25th was aided by a broken treadmill.  It was WAY too easy, especially after weeks away from the gym. In any case, 2 weeks later I've finally huffed, puffed, and cursed my way down to 16:30.  Patting myself on the back as I type this.

Mar 3— 16:15
I finally squeaked out 16:15 today after being stuck at 16:30 for quite a while.  I wonder whether it's my stamina or pain tolerance that's improved.


Zip File of Longing

Wednesday, December 12, 2012

0

There are a couple poems that I've read many times over, and still continue to read.  Though I know the words by heart, once in a while I have the urge to see them inked on a page.  Or at least, reflected off a screen.  Poems are captured emotions, and actually following their shape, feeling the stresses and pauses in their rhythm, releases their original form.

The distilled longing and sense of vulnerability she's zipped into a mere 3 dozen words never fails to move me.



Elysium is as far as to

Elysium is as far as to
The very nearest Room
If in that Room a Friend await
Felicity or Doom —

What fortitude the Soul contains
That it can so endure
The accent of a coming Foot —
The opening of a Door —



I've seen a couple interpretations of who or what exactly is in the other room.  I read this as a love poem.

In Greek mythology, Elysium is a realm of pleasure where heroes go in the afterlife.  So, heaven can be in the nearest room, if in that room awaits the one of your desires (Friend).  Who's there could bring wonderful bliss (Felicity) or utter despair (Doom).

For, when someone you long for is near, even their lightest step—the "accent of a coming Foot", and the gentlest of entrances—the "opening of a Door", fills you with anticipation and dread so heavy the Soul can hardly endure.  These lines capture so sharply those walking-on-eggshells feelings when you desire someone, who with a careless word would lift you to the clouds, or crush you into a spineless mess beneath their feet.



Filtering ListViews with URL Query String

Saturday, December 08, 2012

17

They say that peering lightyears on lightyears out into space is like looking back in time.  Look far enough, and you can catch the very first moments of the universe.  SharePoint is also a universe of sorts.  It's certainly vast and complex enough, and random enough, too.  Its history is written in a maze of ever more obscure backward compatibility features, and the daring may peer into the gaping darkness.  Perhaps even reach in... and grope nifty things like filtering list views with URL parameters.

This technique isn't used much in SP 2010, but still works for backward compatibility.  You can sort and filter list views by tacking on some URL query string parameters.  It also works on any page with a ListView Webpart.

This post will cover:
  • Basic URL filters
  • Filtering multiple columns
  • Multiple filter values on a single column
  • Wildcard filters
  • Filtering on lookup columns

Basic Filter

For instance, the query string below filters the Orders list view to show only items where Product is equal to Jetpack:


You must refer to columns by their internal name, which may be different from the display name.  Fields with spaces in their display name will certainly have different internal names.  For instance, the internal name for Order Number would likely be Order_x0020_Number.

Filtering Multiple Columns

Filters on additional columns can be appended with

&FilterField2=_____&FilterValue2=_____

And so on.

Multi-value Filters

To apply multiple filter values to a single column, the syntax is a little different:

&FilterName=______&FilterMultiValue=____;____;_____

Separate each filter value with a semi-colon ( ; ).  For instance, show only orders whose Product is Jetpack OR Prismatic Core:



Wildcard Filters

FilterMultiValue also supports partial matches.  Use the asterisk ( * ) symbol to indicate a wild card. For instance,

&FilterName=Product&FilterMultiValue=*jet*

shows all orders whose Product contains the string "jet":

Lookup Column Filters

In the examples above, Product is acutally a lookup field, and I've been filtering using the lookup value.  If the Product names are very long, or we just don't know the names, we'll have to filter by the lookup ID.  That is requires a little tinkering with the list.

First we have to bring the lookup ID over as a projected field.  Go to List Settings and edit the lookup column (Product in this example).  Under Additional Column Settings, make sure the ID field is being brought over.


This adds a field to the list called Product:ID, which we can now use to filter the list views.  Because the display name contains a colon ( : ), the internal name will be different.  In most cases, it will be Product_x003a_ID.

The following filter shows all items whose Product's ID is 4:


Joins and Projections in SharePoint 2010

Friday, December 07, 2012

6

List joins and projections is a new feature in SP 2010 that seems to be seeing very little light of day.  Not surprising, considering it comes with a boatload of limitations and caveats.  But there are some situations where it can come in handy.  In this post I'll go over what they can't do, where they're useful, and how to do some simple joins.

Joins in SharePoint work much like SQL joins.  You have multiple lists that are related, and need to show the data in a combined view.  When there are only two lists, the normal lookup functionality usually suffices.  More than two is where joins come in.  However...

Limitations

  • There is no way to leverage joins and projections in a view through the UI.  CAML must be written.  Few things are more badass than CAMLs, but they're often hairy.  (Get it...? Maaan I crack myself up)
  • Joins can only be defined on lookup columns
  • Projected fields cannot be used to sort or group in the view
  • Dynamic sorting and filtering in listview webparts is also not supported
  • Projected columns do not show up correctly in datasheet view, or when exported to Excel

Use Case

Joins and projections can be very useful for filtering and displaying in a listview data from multiple related lists.  Consider this example where we have three lists with a grandparent-parent-child relationship: Manufacturers, Products, and Orders.  Each is linked to the previous by a lookup column.


Since Orders has a lookup to Products, we can create a list view of orders with their related products.  OOB lookup column functionality in SP 2010 allows us to pull in other Product fields as well, such as SKU.


But, what if we want to include the Manufacturer and Industry?  In the rest of this post I'll go over exactly how to achieve something like this:


Unfortunately, a lookup column cannot be used to pull in another lookup field.  Product:SKU works because SKU is a text field on Products.  Product:Manufacturer isn't supported since Manufacturer is a lookup field.

We could create text fields on Products and use event receivers to populate them whenever Manufacturers/Products are updated.  But that's clunky, especially if we need to capture other Manufacturers fields such as Industry.

Joins and projections offer a more elegant solution.

Join Example

The markup below joins Orders to Products, then Products to Manufacturers:

<Joins>
    <Join Type="LEFT" ListAlias="prd">
        <Eq>
              <FieldRef Name="Product" RefType="Id" />
              <FieldRef List="prd" Name="ID" />
        </Eq>
    </Join>
    <Join Type="LEFT" ListAlias="mftr">
        <Eq>
              <FieldRef List="prd" Name="Manufacturer" RefType="Id" />
              <FieldRef List="mftr" Name="ID" />
        </Eq>
    </Join >
</Joins>

Note that the actual lists involved are not explicitly specified.  Since we'll be creating our view on the Orders list, that is assumed to be the root for the joins.  The join from Orders to Products does not specify Products either.  That is implicitly determined by the fact that we're joining on the Product lookup field.

Each joined list has to be assigned a List Alias, which is how it will be referred to later.

The two <FieldRef> elements in the join condition specify the pair of fields that are being joined:

  • The first refers to the Internal Name of a Lookup column (Product) on the primary list (Orders). In the first join, the primary list is not specified; it is assumed to be the root list (Orders). In the second (and any subsequent joins), the primary list must be specified by it's List Alias (prd). RefType should always be "Id".
  • The second FieldRef specifies the column being joined to. It always refers to the ID column of the target list, specified by it's List Alias.

Projection Example

The markup below creates two projected fields from the joined Manufacturers list:

<ProjectedFields>
    <Field Name="Manufacturer" Type="Lookup" List="mftr" ShowField="Title" />
    <Field Name="ManufacturerIndustry" Type="Lookup" List="mftr" ShowField="Industry" />
</ProjectedFields>

Projected fields must  be type Lookup.  The List attribute refers to the List Alias of a join defined above.  ShowField refers to the field on the joined list to create the projected field from.

Putting it Together

When creating a new list via CAML, the joins and projected fields XML above can be included in the list views definition.  Note that they are not children of the <Query> node, but completely separate elements.

Adding a view to an existing list, however, can only be done using the object model.  The easiest way is to make a new feature and put the view creation code in the feature receiver.

Important: Do not include the root <Joins> and <ProjectedFields> tags when setting SPView.Joins and SPView.ProjectedFields.  That will cause a view compilation error at deployment time.

The SPList.Views.Add(...) method does not have parameters for joins and projected fields markup.  Those are set after creating the view.  This means you also have to add view columns after the view is created, instead of passing them in.  See comments below.

public class JoinDemoEventReceiver : SPFeatureReceiver
{
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPSite site = properties.Feature.Parent as SPSite;

if (site != null)
{
SPList ordersList = site.RootWeb.Lists["Orders"];

// Fields to include in view. Leave this empty. We will add fields after
// creating the view. This is because projected vields are not available yet.
StringCollection fields = new StringCollection();

// View Query, Joins, and Projected Fields markup (from examples above)
string query = "<Where></Where>";
string joins = "<Join Type='LEFT' ListAlias='prd'><Eq><FieldRef Name= .....";
string projection = "<Field Name='Manufacturer' Type='Lookup' List= .....";

// Create new view on Orders list, and set the Joins and ProjectedFields
SPView view = ordersList.Views.Add("Joined Orders",fields,query,100,true,false);
view.Joins = joins;
view.ProjectedFields = projection;

// Add fields to view
view.ViewFields.Add("Title");
view.ViewFields.Add("Product");
view.ViewFields.Add("Product_x003a_SKU");
view.ViewFields.Add("Manufacturer");
view.ViewFields.Add("ManufacturerIndustry");
view.ViewFields.Add("Quantity");

// Save
view.Update();
ordersList.Update();
}
}
}

The Finished Product

Build and deploy the solution, and activate the feature.  If all goes well, you should have a new list view with joined columns from its parent and grandparent lists.  Here's our "Joined Orders" view with related product and manufacturer!


If you modify the view from the UI, the projected fields can be shuffled around, or used to filter the view like normal fields.  However, you can not group or sort by them:

 
The end!


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;
}


And Death Shall Have No Dominion

Monday, February 20, 2012

1

I was nodding off on a trans-Atlantic flight, grinding through endless 90's action flicks--only the occasional exploding car keeping me from becoming a drooling mess--when I came across this little gem of a film called Solaris.  The story moved pretty slowly at times, but overall is very thought-provoking.  Definitely one of the better sci-fi films I've seen--no aliens, no lasers, no bs.

The Dylan Thomas poem in Solaris really blew me away.  I'm not sure whether it celebrates the immortality of the human spirit, or implies the opposite.  Either way, this poem gives me some serious chills.  And we're not talking nerd chills here.

The "death shall have no dominion" refrain sounds like one praying for it to be true, all the while knowing it's not.  Do the religious-sounding contradictions show the spirit transcending maddness, suffering, and even death? Or highlight the futility of an existence where happiness is never without suffering?


And Death Shall Have No Dominion

And death shall have no dominion.
Dead mean naked they shall be one
With the man in the wind and the west moon;
When their bones are picked clean and the clean bones gone,
They shall have stars at elbow and foot;
Though they go mad they shall be sane,
Though they sink through the sea they shall rise again;
Though lovers be lost love shall not;
And death shall have no dominion.

And death shall have no dominion.
Under the windings of the sea
They lying long shall not die windily;
Twisting on racks when sinews give way,
Strapped to a wheel, yet they shall not break;
Faith in their hands shall snap in two,
And the unicorn evils run them through;
Split all ends up they shan't crack;
And death shall have no dominion.

And death shall have no dominion.
No more may gulls cry at their ears
Or waves break loud on the seashores;
Where blew a flower may a flower no more
Lift its head to the blows of the rain;
Though they be mad and dead as nails,
Heads of the characters hammer through daisies;
Break in the sun till the sun breaks down,
And death shall have no dominion