{STATIC} hippo

...on becoming a great developer...
posts - 38, comments - 12, trackbacks - 0

My Links

Archives

Post Categories

Thursday, January 07, 2010

ASP.NET MVC & State Pattern: Choose your view

Problem

I have a model that uses the State pattern.  Some States allow read/write access to the Model’s properties and some are read-only.  I wanted a good way to separate the logic for this from my Views; the View shouldn’t have to decide whether properties are displayed as TextBoxes or inside Divs.

First Thought

At first I was going to create a custom ViewEngine.  That ViewEngine would be responsible for choosing the right View – that is, if the View was called “Edit” but the model was in a readonly state, it would change the View to “ReadOnly”.  However, that just didn’t sound right – why was that responsibility falling on the ViewEngine?  Plus, it’s kind of inflexible – what if some editable Views are called “Edit” but some are called “Details”.

Solution

I ended up with a mixture of a custom ActionFilter and a base Controller class.  Before I show you the code, let me show you the usage:

   1: [AcceptVerbs(HttpVerbs.Get)]
   2: [StateItemEditView("ReadOnly")]
   3: public ActionResult Edit(int id)
   4: {
   5:     var module = _Repository.GetByID(id);
   6:     return View(module);
   7: }

Notice Line 2 – this specifies that:

  1. The current action has editable content
  2. The ReadOnly View name is called “ReadOnly”

The code for the controller, which I use as a base class for my controllers that act upon StateItems:

   1: public class StateItemController : Controller
   2: {
   3:     protected override ViewResult View(string viewName, string masterName, object model)
   4:     {
   5:         object readOnlyViewName;
   6:         if (ControllerContext.Controller.TempData
   7:             .TryGetValue(StateItemEditViewAttribute.key_ReadOnlyView, out readOnlyViewName))
   8:         {
   9:             var stateItem = model as IStateItem;
  10:             if (workflowItem != null && stateItem.CurrentState.IsReadOnly)
  11:                 viewName = readOnlyViewName as string;
  12:         }
  13:         return base.View(viewName, masterName, model);
  14:     }
  15: }

Before returning the View, which has a ViewName string, we want to make sure the ViewName is correct.  We check the Controller’s TempData for a predefined key.  If that key exists, the value will be the name of the View to display for ReadOnly states.  We then check if the Model exists and is a StateItem, and if it’s ReadOnly we change the ViewName before returning the View.

The StateItemEditViewAttribute is just responsible for setting the TempData key:

   1: public class StateItemEditViewAttribute : ActionFilterAttribute
   2: {
   3:     public StateItemEditViewAttribute(string readOnlyView)
   4:     {
   5:         _ReadOnlyView = readOnlyView;
   6:     }
   7:     private string _ReadOnlyView;
   8:     public const string key_ReadOnlyView = "ReadOnlyView";
   9:     public override void OnActionExecuting(ActionExecutingContext filterContext)
  10:     {
  11:         filterContext.Controller.TempData.Add(key_ReadOnlyView, _ReadOnlyView);
  12:     }
  13: }

I’m using this for a binary dicision – Edit/ReadOnly – but this could be used for a lot of other purposes too.

posted @ Friday, January 08, 2010 2:10 PM | Feedback (0) | Filed Under [ ASP.NET MVC ]

Calendar sizes: DateTime vs Nullable SqlDateTime in NHibernate

 

Does size matter?

.NET’s DateTime class supports a range between 01/01/0001 00:00:00 and 12/31/9999 23:59:59.9999999.  Sql Server’s calendar is not quite as big – it starts at 1/1/1753.  Therefore, if you have an object with a DateTime that’s set to DateTime.MinValue (or any other date before 1/1/1753) and you try to save that to a database, you’ll get the following exception:

SqlDateTime overflow. Must be between 1/1/1753 12:00:00 AM and 12/31/9999 11:59:59 PM.

This recently got in my way.  I’m building an application on top of a legacy database.  There’s a table with a CreatedDate column that’s nullable.  However, in my domain model I didn’t want CreatedDate to be a Nullable<DateTime> because everything should have a CreatedDate – is there anything that wasn’t created at some point in time?

My object looks like this:

   1: public class MyObject
   2: {
   3:     public MyObject()
   4:     {
   5:         CreatedDate = DateTime.Now;
   6:     }
   7:  
   8:     public virtual string Name { get; set; }
   9:     public virtual DateTime CreatedDate { get; protected set; }
  10: }

Existing data in legacy database

The problem is that many of the existing entries in the database have a NULL entry for CreatedDate.  Because of this, CreatedDate will be set to DateTime.MinValue.  When I tried to save the object back to the database I got the above mentioned SqlDateTime overflow exception.

Going forward I don’t want any more NULL entries, but I don’t have the ability to update all existing NULLs to some arbitrary date (without having a whole back and forth with the DBAs).  What I decided would be the best idea would be to support NULL entries in the DB transparently – NULL entries will map to DateTime.MinValue on the GET side, and DateTime.MinValue will map to NULL on the UPDATE/SAVE side.  Since a new object always has its CreatedDate property set to DateTime.Now on instantiation, only really old objects will show 1/1/0001 as their created date in the system.

Implementation of IUserType

NHibernate has an IUserType interface which allows you to specify your own type for saving and retrieving data.  Here’s what I wrote to handle my requirements:

   1: public class NullDateTimeAsMinDateType : IUserType
   2:     {
   3:         public new bool Equals(object x, object y)
   4:         {
   5:             if (ReferenceEquals(x, y))
   6:                 return true;
   7:             if (x == null || y == null)
   8:                 return false;
   9:             return x.Equals(y);
  10:         }
  11:         #region IUserType Members
  12:  
  13:         public object Assemble(object cached, object owner)
  14:         {
  15:             return cached;
  16:         }
  17:  
  18:         public object DeepCopy(object value)
  19:         {
  20:             return value;
  21:         }
  22:  
  23:         public object Disassemble(object value)
  24:         {
  25:             return value;
  26:         }
  27:  
  28:         public int GetHashCode(object x)
  29:         {
  30:             return x == null ? typeof(DateTime).GetHashCode() : x.GetHashCode();
  31:         }
  32:  
  33:         public bool IsMutable
  34:         {
  35:             get { return false; }
  36:         }
  37:  
  38:         public object NullSafeGet(System.Data.IDataReader rs, string[] names, object owner)
  39:         {
  40:             var obj = NHibernateUtil.DateTime.NullSafeGet(rs, names[0]);
  41:             if (obj == null)
  42:                 return DateTime.MinValue;
  43:  
  44:             DateTime dateTime = (DateTime) obj;
  45:             return dateTime;
  46:         }
  47:  
  48:         public void NullSafeSet(System.Data.IDbCommand cmd, object value, int index)
  49:         {
  50:             if (value == null)
  51:             {
  52:                 ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
  53:                 return;
  54:             }
  55:             DateTime dateTime = (DateTime)value;
  56:             if (dateTime.CompareTo(System.Data.SqlTypes.SqlDateTime.MinValue.Value) < 0)
  57:                 ((IDataParameter)cmd.Parameters[index]).Value = DBNull.Value;
  58:             else
  59:                 ((IDataParameter)cmd.Parameters[index]).Value = dateTime;
  60:         }
  61:  
  62:         public object Replace(object original, object target, object owner)
  63:         {
  64:             return original;
  65:         }
  66:  
  67:         public Type ReturnedType
  68:         {
  69:             get { return typeof(DateTime); }
  70:         }
  71:  
  72:         public NHibernate.SqlTypes.SqlType[] SqlTypes
  73:         {
  74:             get { return new[] { NHibernateUtil.DateTime.SqlType }; }
  75:         }
  76:  
  77:         #endregion
  78:     }

 

I’m using FluentNHibernate, so my mapping looks like:

   1: Map(x => x.CreateDDate)
   2:     .Column("create_date")
   3:     .CustomType<NullDateTimeAsMinDateType>()
   4:     .Nullable();

 

That’s it.  Now the CreatedDate column in the DB doesn’t have to get touched and SQL Server’s date fields play nice with .NET DateTime.

posted @ Thursday, January 07, 2010 1:06 PM | Feedback (1) |

Powered by:
Powered By Subtext Powered By ASP.NET