I'm running into a problem where an ASP.NET MVC html helper method I created is not being "regenerated" each time it is called.
The purpose of the helper method is to create Javascript objects to be used in an angularjs framework. For example, here's a code snippet where the helper method is used (called from within a script tag of an html page):
var app = angular.module( "appName", ["ui.bootstrap"] );
app.controller( 'appCtrl', function( $scope ) {
$scope.model = @Html.ToJavascript( Model, new string[] { "FirstName", "LastName", "ID", "Role" } );
} );
Model is an instance of a class which has a variety of properties, but I only want FirstName, LastName, ID and Role to get serialized to a javascript object.
The ToJavascript() helper method is defined in a statis class as follows:
public static HtmlString ToJavascript( this HtmlHelper helper, object toConvert, string[] includedFields = null, Formatting formatting = Formatting.Indented, ReferenceLoopHandling loopHandling = ReferenceLoopHandling.Ignore )
{
using( var stringWriter = new StringWriter() )
using( var jsonWriter = new JsonTextWriter( stringWriter ) )
{
var serializer = new JsonSerializer()
{
// Let's use camelCasing as is common practice in JavaScript
ContractResolver = new SpecificFieldsResolver( includedFields ),
Formatting = formatting,
ReferenceLoopHandling = loopHandling,
};
// We don't want quotes around object names
jsonWriter.QuoteName = false;
serializer.Serialize( jsonWriter, toConvert );
return new HtmlString( stringWriter.ToString() );
}
}
This utilizes Json.NET to do the actual serialization.
One of the many cool features of Json.NET is that it lets you define, on the fly, which fields get serialized. That's what the SpecificFieldsResolver does. I've defined it as follows:
public class SpecificFieldsResolver : CamelCasePropertyNamesContractResolver
{
private string[] _included;
public SpecificFieldsResolver( string[] included )
{
_included = included;
}
protected override JsonProperty CreateProperty( MemberInfo member, MemberSerialization memberSerialization )
{
JsonProperty prop = base.CreateProperty( member, memberSerialization );
bool inclField = ( _included == null )
|| _included.Contains( member.Name, StringComparer.CurrentCultureIgnoreCase );
prop.ShouldSerialize = obj => inclField;
return prop;
}
}
What's confusing me is the way that CreateProperty() gets called. Specifically, it seems to only get called once for each type of object being serialized.
That's a problem because in another cshtml file I have another call to ToJavascript() which is attempting to serialize the same type of object, but with different fields to be output from the serialization:
var app = angular.module( "app2Name", ["ui.bootstrap"] );
app.controller( 'app2Ctrl', function( $scope ) {
$scope.model = @Html.ToJavascript( Model, new string[] { "FirstName", "LastName", "ID", "Role", "Category", "VoterID" } );
} );
Category and VoterID are also valid class fields. But ToJavascript() doesn't seralize them. Instead, it only serializes the fields defined in the first call to ToJavascript()...even though that call takes place in a different cshtml file. It's as if SpecificFieldsResolver remembers the JsonProperty objects it creates.
Thoughts?
Update
Thanx to dbc for diagnosing exactly what was wrong and suggesting a workaround. I adapated it slightly because I rely on Json.NET's camel case name resolution in several resolvers:
public class CamelCaseNameMapper : CamelCasePropertyNamesContractResolver
{
public string ToCamelCase( string propertyName )
{
return ResolvePropertyName( propertyName );
}
}
public class MaoDefaultContractResolver : DefaultContractResolver
{
private CamelCaseNameMapper _mapper = new CamelCaseNameMapper();
protected override string ResolvePropertyName( string propertyName )
{
return _mapper.ToCamelCase( propertyName );
}
}
Now every resolver, such as my SpecificFieldsResolver, which derives from MaoDefaultContractResolver automatically inherits camel casing but avoids the caching problem the dbc identified.
This appears to be a bug with
CamelCasePropertyNamesContractResolver
. Its base class,DefaultContractResolver
, has two constructors: a parameterless constructor, and aDefaultContractResolver (Boolean)
version (just made obsolete in Json.NET 7.0). This parameter has the following meaning:The default is
false
.Unfortunately, the default constructor for
CamelCasePropertyNamesContractResolver
sets the value totrue
:Further, there is no second constructor with the
shareCache
option. This breaks yourSpecificFieldsResolver
.As a workaround, you could derive your resolver from
DefaultContractResolver
and useCamelCaseNamingStrategy
to do the name mapping:Note that if you are using a version of Json.NET prior to 9.0,
CamelCaseNamingStrategy
does not exist. Instead a kludge nestedCamelCasePropertyNamesContractResolver
can be used to map the names: