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

21

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!