Wednesday, November 23, 2011

Serializing dates in MVC (update)

As I was falling asleep in bed last night, a thought went through my mind: "Since I'm using this Regular Expression every time I load the page, isn't it rather wasteful to be compiling it every time?" So when I got up this morning, I looked up "Compiled Regular Expressions", and read through this page: http://www.dijksterhuis.org/regular-expressions-advanced/. Not only did I pick up some tips on compiling regular expressions, I also discovered a much simpler (and likely quite more efficient) way of replacing the MS-formatted date with my date string. In my previous method, I was casting the Regex matches to an IENumerable and using Linq to iterate through them; in the updated version, the iterative process is handled internally by the Regex object. All I have to do is tell it to use my FixDates method whenever it encounters a match. In the process of updating my code, I also added support for custom Date Formats and set the default to "yyyy-MM-dd".
private static readonly Regex DateRegex = new Regex(@"\\/Date\((?<ticks>\d+)?\)\\/", RegexOptions.Compiled);
/// <summary>
/// If true, date-specific serialization code will be run;
/// If false, date-specific serialization code will not be run.
/// </summary>
public bool HasDates { get; set; }
/// <summary>
/// The date format string to be applied. Defaults to "yyyy-MM-dd".
/// </summary>
public string DateFormatString { get; set; }

/// <summary>
/// Replaces MS-formatted dates with date strings
/// </summary>
/// <param name="match"></param>
/// <returns></returns>
public string FixDates(Match match)
{
    return new DateTime(1970, 1, 1)
        .AddMilliseconds(long.Parse(match.Groups["ticks"].Value))
        .ToString(DateFormatString);
}

/// <summary>
/// Serialize the object to JavaScript and
/// perform extra formatting on the serialized string as necessary
/// </summary>
/// <returns></returns>
public string Serialize()
{
    JavaScriptSerializer serializer = new JavaScriptSerializer();
    var serializedData = serializer.Serialize(Data);
    if (HasDates)
        serializedData = DateRegex.Replace(serializedData, FixDates);

    return serializedData;
}
Click here to download the updated code.

Monday, November 21, 2011

Serializing dates in MVC

Update: I've rewritten the code for simplicity and efficiency. You can check it out here.
Ah, dates. Simple little things, really, especially when working with .Net and JavaScript, right?
Well, sort of. See, Microsoft decided that when serializing DateTime objects, they should be formatted like this:
"\/Date(12345678)\/". Unfortunately, without running eval() on that, that format is useless.
I've never really had this issue before, because when working with dates on the client side, I always use strings.
So why the issue now?
Because, along with using MVC, I decided to use the Entity Framework, which made it really easy to return a bunch of objects from the database:
return Json(db.Tasks.ToList());
In the past, this would have been more like:

DataTable dt = new DataTable();
dt.Load(command.ExecuteReader());
return dt.Rows.Cast<DataRow>().Select(
                r =>
                new
                    {
                        id = (int) r["id"],
                        name = (string) r["name"],
                        date = ((DateTime) r["date"]).ToString("yyyy-MM-dd")
                    });

Now, I could simply go on using the select statement, but I just couldn't bear the thought of doing that everytime I had to work with dates (since that is all the time). So instead I wrote a custom Json serializer derived from JsonResult. I've uploaded the source code (link at bottom), but the real magic is in these lines:

public bool HasDates { get; set; }

public override void ExecuteResult(ControllerContext context)
{ 
    //... a bunch of code ...
    if (Data != null)
    {
        JavaScriptSerializer serializer = new JavaScriptSerializer();
        response.Write(HasDates ? FixDates(serializer.Serialize(Data)) : serializer.Serialize(Data));
    }
}   

public string FixDates(string data)
{
    var matches = Regex.Matches(data, @"\\/Date\((?<ticks>\d+)?\)\\/").Cast<Match>().ToArray();
    for (int i = matches.Length - 1; i >= 0; i--)
    {
        var match = matches[i];
        data = data.Remove(match.Index, match.Length)
            .Insert(match.Index,
                    new DateTime(1970, 1, 1).AddMilliseconds(long.Parse(match.Groups["ticks"].Value))
                        .ToString("yyyy-MM-dd HH:mm:ss"));
    }
    return data;
}

The code simply loops through the standard serialized result and replaces any MS-formatted date strings with my kind of date string (yyyy-MM-dd HH:mm:ss eg. "2011-11-21 19:48:13").

Updated return call:
return new ProperJsonResult() { HasDates = true, Data = db.Tasks.ToList() };

Click here to download ProperJsonResult.cs