Saturday 17 April 2010

Using the EditorFor method in MVC2 to display child collections

I was very excited when I discovered the new DisplayFor and EditorFor extensions in the new MVC release. In fact, they look even better than the similar MVCContrib features.

The only problem is that when I tried to render a child collection, it displayed nothing. This is kinda weird, because when your model is a collection, it shows just fine. However, as this post explains, anything complex enough (that is, not convertible to a string) is just filtered out.

It turned out that it could be fixed relatively easily. You have to override the default Object template and fix the logic. Just create a view called Object.ascx in your Views/Shared/EditorTemplates folder, copy the default template from here, and modify the filtering logic. Took me a while, since I had to convert it to VB.Net. Here's the result:

<%@ Control Language="VB" Inherits="System.Web.Mvc.ViewUserControl" %>
<%@ Import Namespace="System.Globalization"%>
<%@ Import Namespace="System.Linq" %>

<% If (ViewData.ModelMetadata.Model Is Nothing) Then%>
<%=ViewData.ModelMetadata.NullDisplayText%>
<% Else%>
<% If (ViewData.TemplateInfo.TemplateDepth > 3) Then%>
<%=Me.ViewData.ModelMetadata.SimpleDisplayText%>
<% Else%>
<% Dim props = From pm In Me.ViewData.ModelMetadata.Properties Where pm.ShowForEdit AndAlso Not ViewData.TemplateInfo.Visited(pm) For Each prop In props If prop.HideSurroundingHtml Then%>
<%= Html.Editor(prop.PropertyName) %>
<% Else Dim str As String = Html.Label(prop.PropertyName).ToHtmlString If Not String.IsNullOrEmpty(str) Then%>
<%= Html.Label(prop.PropertyName) %>

<% End If%>

<%= Html.Editor(prop.PropertyName) %>
<%= Html.ValidationMessage(prop.PropertyName, "*") %>

<% End If%>


<%Next%>
<% End If%>
<% End If%>

Don't forget to check the line related to TemplateDepth. Originally, it makes a shallow copy: everything with the depth>1 is just displayed as a simple string. You might want to make this number greater, or remove the check entirely.

Friday 9 April 2010

Using the latest NHibernate features in your VB.Net code.

No doubt you have tried the newest LINQ-to-NHibernate bits in your VB.Net application already. If you have gotten past retrieving all records from a specific table, you might been getting an exception similar to this one:

System.Exception: Unrecognised method call in epression CompareString(aUser.Name, value(Web.Controllers.Membership.AccountMembershipService+_Closure$__1).$VB$Local_userName, False)
at NHibernate.Impl.ExpressionProcessor.FindMemberExpression(Expression expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 170
at NHibernate.Impl.ExpressionProcessor.ProcessSimpleExpression(BinaryExpression be) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 295
at NHibernate.Impl.ExpressionProcessor.ProcessBinaryExpression(BinaryExpression expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 358
at NHibernate.Impl.ExpressionProcessor.ProcessExpression(Expression expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 388
at NHibernate.Impl.ExpressionProcessor.ProcessLambdaExpression(LambdaExpression expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 395
at NHibernate.Impl.ExpressionProcessor.ProcessExpression[T](Expression`1 expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Impl\ExpressionProcessor.cs:line 406
at NHibernate.Criterion.QueryOver`2.Add(Expression`1 expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Criterion\QueryOver.cs:line 568
at NHibernate.Criterion.QueryOver`2.Where(Expression`1 expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Criterion\QueryOver.cs:line 248
at NHibernate.Criterion.QueryOver`2.NHibernate.IQueryOver.Where(Expression`1 expression) in f:\Разные штуки\Visual Studio 2008\Projects\OpenSource\NHibernate\nhibernate\src\NHibernate\Criterion\QueryOver.cs:line 619

The code I have is quite innocent:
Return _session.QueryOver(Of User).Where(_
   Function(aUser) aUser.Name = userName).List().First

It returns the first instance of the User class that matches the Name property. The problem is that the VB compiler transforms the lambda into something like
(CompareString(aUser.Name, value(Web.Controllers.Membership.AccountMembershipService+_Closure$__1).$VB$Local_userName, False) = 0)

Now, CompareString is a shared method of the Microsoft.VisualBasic.CompilerServices.Operators class, which is probably not something the NHibernate developers expected to appear in a lambda. Time for a patch. A quick fix is to add the following piece of code in the beginning of the NHibernate.Impl.ExpressionProcessor.ProcessSimpleExpression method:

if (be.Left.NodeType == ExpressionType.Call) {
   var left = (MethodCallExpression) be.Left;
   if (left.Method.Name == "CompareString") {
      return ProcessSimpleExpression(Expression.Equal(left.Arguments[0], left.Arguments[1]));
   }
}

Actually, the CompareString method has 3 arguments. The third one, when set to false, signifies the literal comparison, while being set to true means textual (case insensitive) comparison. So, if you use the LIKE operator, you might want to check for left.Arguments[2].