AngularJS passing Json to controller and displaying in a DevExpress Chart - json

I have a JsonResult method in my controller. I have pulled in data from my database and set to an object. The method will return this object. I am trying pass this data into AngularJS data source. I would like to display a DevExtreme bar chart. Here is code so far.
AngularJS file:
var app = angular.module('customApp', ['dx']);
app.controller("chartControl", function ($scope, $http) {
$scope.sizeSettings = {
dataSource: 'http://localhost:53640/Home/PostChart',
commonSeriesSettings: {
argumentField: 'product_id',
valueField: "product_id", name: "Product Cost",
type: "bar"
},
seriesTemplate: {
nameField: 'Source',
}
};
});
Home Controller:
public JsonResult PostChart(int product_id)
{
Object prod = null;
using (ProductOrderEntities db = new ProductOrderEntities())
{
var product = db.Products.FirstOrDefault(p => p.product_id == product_id);
prod = new {productID = product.product_id, productName = product.product_name, productPrice = product.product_cost, productDescription = product.product_type};
}
return Json(prod, JsonRequestBehavior.AllowGet);
}
}
HTML
<div ng-app="customApp">
<div ng-controller="chartControl">
</div>
</div>

Seems like you forget add markup for chart:
<div dx-chart="chartOptions"></div>
I've made a simple ASP.NET MVC application here https://www.dropbox.com/s/hk3viceoa2zkyng/DevExtremeChart.zip?dl=0
Open the start page http://localhost:56158/Default1/ to see chart in action.
See more information about using DevExtreme Charts in AngularJS app here http://js.devexpress.com/Documentation/Howto/Data_Visualization/Basics/Create_a_Widget/?version=14_2#Data_Visualization_Basics_Create_a_Widget_Add_a_Widget_AngularJS_Approach

This is how I solved it.
HTML
<div ng-app="customCharts">
<div ng-controller="ChartController">
<div dx-chart="productSettings"></div>
</div>
</div>
AngularJS
var app = angular.module('customCharts', ['dx']);
app.controller("ChartController", function ($scope, $http, $q) {
$scope.productSettings = {
dataSource: new DevExpress.data.DataSource({
load: function () {
var def = $.Deferred();
$http({
method: 'GET',
url: 'http://localhost:53640/Home/PostChart'
}).success(function (data) {
def.resolve(data);
});
return def.promise();
}
}),
series: {
title: 'Displays Product Costs for items in our Database',
argumentType: String,
argumentField: "Name",
valueField: "Cost",
type: "bar",
color: '#008B8B'
},
commonAxisSettings: {
visible: true,
color: 'black',
width: 2
},
argumentAxis: {
title: 'Items in Product Store Database'
},
valueAxis: {
title: 'Dollor Amount'
}
}
})
Controller
public JsonResult PostChart()
{
var prod = new List<Object>();
using (ProductOrderEntities db = new ProductOrderEntities())
{
var product = db.Products.ToList();
foreach (var p in product)
{
var thing = new { Name = p.product_name, Cost = p.product_cost };
prod.Add(thing);
}
}
return Json(prod, JsonRequestBehavior.AllowGet);
}

Related

Transform Request to Autoquery friendly

We are working with a 3rd party grid (telerik kendo) that has paging/sorting/filtering built in. It will send the requests in a certain way when making the GET call and I'm trying to determine if there is a way to translate these requests to AutoQuery friendly requests.
Query string params
Sort Pattern:
sort[{0}][field] and sort[{0}][dir]
Filtering:
filter[filters][{0}][field]
filter[filters][{0}][operator]
filter[filters][{0}][value]
So this which is populated in the querystring:
filter[filters][0][field]
filter[filters][0][operator]
filter[filters][0][value]
would need to be translated to.
FieldName=1 // filter[filters][0][field]+filter[filters][0][operator]+filter[filters][0][value] in a nutshell (not exactly true)
Should I manipulate the querystring object in a plugin by removing the filters (or just adding the ones I need) ? Is there a better option here?
I'm not sure there is a clean way to do this on the kendo side either.
I will explain the two routes I'm going down, I hope to see a better answer.
First, I tried to modify the querystring in a request filter, but could not. I ended up having to run the autoqueries manually by getting the params and modifying them before calling AutoQuery.Execute. Something like this:
var requestparams = Request.ToAutoQueryParams();
var q = AutoQueryDb.CreateQuery(requestobject, requestparams);
AutoQueryDb.Execute(requestobject, q);
I wish there was a more global way to do this. The extension method just loops over all the querystring params and adds the ones that I need.
After doing the above work, I wasn't very happy with the result so I investigated doing it differently and ended up with the following:
Register the Kendo grid filter operations to their equivalent Service Stack auto query ones:
var aq = new AutoQueryFeature { MaxLimit = 100, EnableAutoQueryViewer=true };
aq.ImplicitConventions.Add("%neq", aq.ImplicitConventions["%NotEqualTo"]);
aq.ImplicitConventions.Add("%eq", "{Field} = {Value}");
Next, on the grid's read operation, we need to reformat the the querystring:
read: {
url: "/api/stuff?format=json&isGrid=true",
data: function (options) {
if (options.sort && options.sort.length > 0) {
options.OrderBy = (options.sort[0].dir == "desc" ? "-" : "") + options.sort[0].field;
}
if (options.filter && options.filter.filters.length > 0) {
for (var i = 0; i < options.filter.filters.length; i++) {
var f = options.filter.filters[i];
console.log(f);
options[f.field + f.operator] = f.value;
}
}
}
Now, the grid will send the operations in a Autoquery friendly manner.
I created an AutoQueryDataSource ts class that you may or may not find useful.
It's usage is along the lines of:
this.gridDataSource = AutoQueryKendoDataSource.getDefaultInstance<dtos.QueryDbSubclass, dtos.ListDefinition>('/api/autoQueryRoute', { orderByDesc: 'createdOn' });
export default class AutoQueryKendoDataSource<queryT extends dtos.QueryDb_1<T>, T> extends kendo.data.DataSource {
private constructor(options: kendo.data.DataSourceOptions = {}, public route?: string, public request?: queryT) {
super(options)
}
defer: ng.IDeferred<any>;
static exportToExcel(columns: kendo.ui.GridColumn[], dataSource: kendo.data.DataSource, filename: string) {
let rows = [{ cells: columns.map(d => { return { value: d.field }; }) }];
dataSource.fetch(function () {
var data = this.data();
for (var i = 0; i < data.length; i++) {
//push single row for every record
rows.push({
cells: _.map(columns, d => { return { value: data[i][d.field] } })
})
}
var workbook = new kendo.ooxml.Workbook({
sheets: [
{
columns: _.map(columns, d => { return { autoWidth: true } }),
// Title of the sheet
title: filename,
// Rows of the sheet
rows: rows
}
]
});
//save the file as Excel file with extension xlsx
kendo.saveAs({ dataURI: workbook.toDataURL(), fileName: filename });
})
}
static getDefaultInstance<queryT extends dtos.QueryDb_1<T>, T>(route: string, request: queryT, $q?: ng.IQService, model?: any) {
let sortInfo: {
orderBy?: string,
orderByDesc?: string,
skip?: number
} = {
};
let opts = {
transport: {
read: {
url: route,
dataType: 'json',
data: request
},
parameterMap: (data, type) => {
if (type == 'read') {
if (data.sort) {
data.sort.forEach((s: any) => {
if (s.field.indexOf('.') > -1) {
var arr = _.split(s.field, '.')
s.field = arr[arr.length - 1];
}
})
}//for autoquery to work, need only field names not entity names.
sortInfo = {
orderByDesc: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'desc'), 'field'), ','),
orderBy: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'asc'), 'field'), ','),
skip: 0
}
if (data.page)
sortInfo.skip = (data.page - 1) * data.pageSize,
_.extend(data, request);
//override sorting if done via grid
if (sortInfo.orderByDesc) {
(<any>data).orderByDesc = sortInfo.orderByDesc;
(<any>data).orderBy = null;
}
if (sortInfo.orderBy) {
(<any>data).orderBy = sortInfo.orderBy;
(<any>data).orderByDesc = null;
}
(<any>data).skip = sortInfo.skip;
return data;
}
return data;
},
},
requestStart: (e: kendo.data.DataSourceRequestStartEvent) => {
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if ($q)
ds.defer = $q.defer();
},
requestEnd: (e: kendo.data.DataSourceRequestEndEvent) => {
new DatesToStringsService().convert(e.response);
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if (ds.defer)
ds.defer.resolve();
},
schema: {
data: (response: dtos.QueryResponse<T>) => {
return response.results;
},
type: 'json',
total: 'total',
model: model
},
pageSize: request.take || 40,
page: 1,
serverPaging: true,
serverSorting: true
}
let ds = new AutoQueryKendoDataSource<queryT, T>(opts, route, request);
return ds;
}
}

How do i test my custom angular schema form field

I've just started developing with Angular schema form and I'm struggling to write any tests for my custom field directive.
I've tried compiling the schema form html tag which runs through my directives config testing it's display conditions against the data in the schema. However it never seems to run my controller and I can't get a reference to the directives HTML elements. Can someone give me some guidance on how to get a reference to the directive? Below is what I have so far:
angular.module('schemaForm').config(['schemaFormProvider',
'schemaFormDecoratorsProvider', 'sfPathProvider',
function(schemaFormProvider, schemaFormDecoratorsProvider, sfPathProvider) {
var date = function (name, schema, options) {
if (schema.type === 'string' && schema.format == 'date') {
var f = schemaFormProvider.stdFormObj(name, schema, options);
f.key = options.path;
f.type = 'date';
options.lookup[sfPathProvider.stringify(options.path)] = f;
return f;
}
};
schemaFormProvider.defaults.string.unshift(date);
schemaFormDecoratorsProvider.addMapping('bootstrapDecorator', 'date',
'app/modules/json_schema_form/schema_form_date_picker/schema_form_date_picker.html');
}]);
var dateControllerFunction = function($scope) {
$scope.isCalendarOpen = false;
$scope.showCalendar = function () {
$scope.isCalendarOpen = true;
};
$scope.calendarSave = function (date) {
var leaf_model = $scope.ngModel[$scope.ngModel.length - 1];
var formattedDate = $scope.filter('date')(date, 'yyyy-MM-dd');
leaf_model.$setViewValue(formattedDate);
$scope.isCalendarOpen = false;
};
};
angular.module('schemaForm').directive('schemaFormDatePickerDirective', ['$filter', function($filter) {
return {
require: ['ngModel'],
restrict: 'A',
scope: false,
controller : ['$scope', dateControllerFunction],
link: function(scope, iElement, iAttrs, ngModelCtrl) {
scope.ngModel = ngModelCtrl;
scope.filter = $filter
}
};
}]);
<div ng-class="{'has-error': hasError()}">
<div ng-model="$$value$$" schema-form-date-picker-directive>
<md-input-container>
<!-- showTitle function is implemented by ASF -->
<label ng-show="showTitle()">{{form.title}}</label>
<input name="dateTimePicker" ng-model="$$value$$" ng-focus="showCalendar()" ng-disabled="isCalendarOpen">
</md-input-container>
<time-date-picker ng-model="catalogue.effectiveFrom" ng-if="isCalendarOpen" on-save="calendarSave($value)" display-mode="date"></time-date-picker>
</div>
<!-- hasError() defined by ASF -->
<span class="help-block" sf-message="form.description"></span>
</div>
And the spec:
'use strict'
describe('SchemaFormDatePicker', function() {
var $compile = undefined;
var $rootScope = undefined;
var $scope = undefined
var scope = undefined
var $httpBackend = undefined;
var elem = undefined;
var html = '<form sf-schema="schema" sf-form="form" sf-model="schemaModel"></form>';
var $templateCache = undefined;
var directive = undefined;
beforeEach(function(){
module('app');
});
beforeEach(inject(function(_$compile_, _$rootScope_, _$templateCache_, _$httpBackend_) {
$compile = _$compile_
$rootScope = _$rootScope_
$httpBackend = _$httpBackend_
$templateCache = _$templateCache_
}));
beforeEach(function(){
//Absorb call for locale
$httpBackend.expectGET('assets/locale/en_gb.json').respond(200, {});
$templateCache.put('app/modules/json_schema_form/schema_form_date_picker/schema_form_date_picker.html', '');
$scope = $rootScope.$new()
$scope.schema = {
type: 'object',
properties: {
party: {
title: 'party',
type: 'string',
format: 'date'
}}};
$scope.form = [{key: 'party'}];
$scope.schemaModel = {};
});
describe("showCalendar", function () {
beforeEach(function(){
elem = $compile(html)($scope);
$scope.$digest();
$httpBackend.flush();
scope = elem.isolateScope();
});
it('should set isCalendarOpen to true', function(){
var result = elem.find('time-date-picker');
console.log("RESULT: "+result);
));
});
});
});
If you look at the below example taken from the project itself you can see that when it uses $compile it uses angular.element() first when setting tmpl.
Also, the supplied test module name is 'app' while the code sample has the module name 'schemaForm'. The examples in the 1.0.0 version of Angular Schema Form repo all use sinon and chai, I'm not sure what changes you would need to make if you do not use those.
Note: runSync(scope, tmpl); is a new addition for 1.0.0 given it is now run through async functions to process $ref includes.
/* eslint-disable quotes, no-var */
/* disabling quotes makes it easier to copy tests into the example app */
chai.should();
var runSync = function(scope, tmpl) {
var directiveScope = tmpl.isolateScope();
sinon.stub(directiveScope, 'resolveReferences', function(schema, form) {
directiveScope.render(schema, form);
});
scope.$apply();
};
describe('sf-array.directive.js', function() {
var exampleSchema;
var tmpl;
beforeEach(module('schemaForm'));
beforeEach(
module(function($sceProvider) {
$sceProvider.enabled(false);
exampleSchema = {
"type": "object",
"properties": {
"names": {
"type": "array",
"description": "foobar",
"items": {
"type": "object",
"properties": {
"name": {
"title": "Name",
"type": "string",
"default": 6,
},
},
},
},
},
};
})
);
it('should not throw needless errors on validate [ノಠ益ಠ]ノ彡┻━┻', function(done) {
tmpl = angular.element(
'<form name="testform" sf-schema="schema" sf-form="form" sf-model="model" json="{{model | json}}"></form>'
);
inject(function($compile, $rootScope) {
var scope = $rootScope.$new();
scope.model = {};
scope.schema = exampleSchema;
scope.form = [ "*" ];
$compile(tmpl)(scope);
runSync(scope, tmpl);
tmpl.find('div.help-block').text().should.equal('foobar');
var add = tmpl.find('button').eq(1);
add.click();
$rootScope.$apply();
setTimeout(function() {
var errors = tmpl.find('.help-block');
errors.text().should.equal('foobar');
done();
}, 0);
});
});
});

Pass data from controller to view via JSONResult

I'd like to send ViewModel from controller to view in JSON format.
Controller:
public ActionResult Select(int pageLimiter)
{
var viewModel = new ArticlesViewModel
{
Articles = this.Service.GetArticles(0, 0, 0),
ArticlesTotal = this.Service.CountArticles(0),
Pages = new List<string>
{
"1", "2", "3"
}
};
return Json(viewModel, JsonRequestBehavior.AllowGet);
}
View:
<ul class="articleList">
#if (#Model != null)
{
foreach (var article in #Model.Articles)
{
<li>
<header>#article.Title</header>
<nav>
<span>#article.AuthorName</span> |
<time>#article.PublishDate.ToString("")</time> |
<span>#article.CategoryName</span> |
<span>#article.Comments Comments</span>
</nav>
<section>
#article.Content
</section>
</li>
}
}
</ul>
<script type="text/javascript">
$(document).ready(function () {
GetArticles(5);
$("#selectPager").change(function () {
var selectedItem = "";
$("#selectPager option:selected").each(function () {
selectedItem = $(this).text();
});
GetArticles(selectedItem);
});
});
function GetArticles(pageLimitValue) {
$.ajax(
{
url: "/Articles/Select",
dataType: "json",
data: { pageLimiter: pageLimitValue },
async: true,
beforeSend: function () {
alert("before");
},
complete: function (data) {
#Model = SOME_MAGIC_TRICKS
}
});
}
As you can see, in the complete event are words SOME_MAGIC_TRICKS. In this place I'd like to set #Model obtained from controller. Is it possible at all? How to insert data from ajax result to view model (it's null by default)?
You are trying to modify server variable from client's code. It's not possible.
If you want to re-render your page's content on complete, you may render <ul class="articleList"> with PartialView and return same partial view instead of JsonResult. Further, oncomplete handler will update your <ul class="articleList"> with returned content.
You can send data doing serialize it may be like:
public ActionResult Select(int pageLimiter)
{
var viewModel = new ArticlesViewModel
{
Articles = this.Service.GetArticles(0, 0, 0),
ArticlesTotal = this.Service.CountArticles(0),
Pages = new List<string>
{
"1", "2", "3"
}
};
string myjsonmodel = new JavaScriptSerializer().Serialize(viewModel );
return Json(jsonmodel = viewModel, JsonRequestBehavior.AllowGet);
}
dont forget using using System.Web.Script.Serialization;
Edit:
To deserialize object try this:
#{
JavaScriptSerializer jss= new JavaScriptSerializer();
User user = jss.Deserialize<User>(jsonResponse);
}

JqGrid trying to send large data from server to grid but getting:Error during serialization or deserialization using the JSON JavaScriptSerializer

I have a problem I am receiving large amount of data from the server and am then converting it to Json format, to be then viewed in JqGrid. It works for small amount of data say for example 200 rows but when doing this for 10000 rows it throws the following error
System.InvalidOperationException: Error during serialization or deserialization using the JSON JavaScriptSerializer. The length of the string exceeds the value set on the maxJsonLength property
I have tried using the javascript serializer and set it to maxjsonLenght = int32.MaxValue but still no luck
Following is my code please give me suggestions with examples how I can fix this? Thanks all!
GridConfig
public JqGridConfig(String db, String jobGroup, String jobName, String detailTable, String filterBatchControl, String filterDate, String filterTime, int page)
{
var entityhelper = new EntityHelper();
var s = new JsonSerializer();
try
{
//Populate Grid Model, Column Names, Grid Column Model, Grid Data
entityhelper.PopulateDetailGridInit(db, jobGroup, jobName, detailTable, filterBatchControl, filterDate, filterTime);
JqGridDetailAttributes = entityhelper.GridDetailAttributes;
JqGridDetailColumnNames = entityhelper.GridDetailColumnNames;
//JqGridDetailsColumnNamesForExport = entityhelper.GridDetailColumnNamesForExport;
JqGridDetailColumnModel = entityhelper.GridDetailColumnModel;
//Dynamic Data
JqGridDynamicDetailData = entityhelper.GridDetailData;
#region Column Model
foreach (KeyValuePair<String, JqGridColModel> kvp in entityhelper.GridDetailColumnModel)
{
s.Serialize(kvp.Key, kvp.Value.Attributes);
}
JqGridDetailColumnModelJson = s.Json();
#endregion
#region Concrete data. 1. List<dynamic> populated, 2. Convert to Json String, 3: Convert back to List<Detail>
JqGridDetailData = JsonSerializer.ConvertDynamicDetailsToJson(JqGridDynamicDetailData); // this is where the error occurs
}
catch (Exception ex)
{
//TODO: Logging
System.Diagnostics.Debug.WriteLine(ex.Message);
}
}
Json Serializer
public static IList<Detail> ConvertDynamicDetailsToJson(IList<dynamic> list)
{
if (list.Count == 0)
return new List<Detail>();
var sb = new StringBuilder();
var contents = new List<String>();
sb.Append("[");
foreach (var item in list)
{
var d = item as IDictionary<String, Object>;
sb.Append("{");
foreach (KeyValuePair<String, Object> kvp in d)
{
contents.Add(String.Format("{0}: {1}", "\"" + kvp.Key + "\"", JsonConvert.SerializeObject(kvp.Value)));
}
sb.Append(String.Join(",", contents.ToArray()));
sb.Append("},");
}
sb.Append("]");
//remove trailing comma
sb.Remove(sb.Length - 2, 1);
var jarray = JsonConvert.DeserializeObject<List<Detail>>(sb.ToString());
return jarray;
}
Controller that return Json result from server
public JsonResult DetailGridData(TheParams param)
{
dynamic config= "";
switch (param.JobGroup)
{
case "a":
config = new BLL.abcBLL().GetDetailGridData("rid", "desc", 1, 20, null,
param.FilterBatchControl,
param.JobName, param.DetailTable,
param.JobGroup, param.BatchDate,
param.Source);
break;
}
return Json(config, JsonRequestBehavior.AllowGet); // this reurns successfully json result
}
View where the Jqgrid exists and does not populate the grid
<script type="text/javascript">
var jobGroup = '#ViewBag.JobGroup';
var jobName = '#ViewBag.JobName';
var detailTable = '#ViewBag.DetailTable';
var filterBatchControl = '#ViewBag.FilterBatchControl';
var controlDate = '#ViewBag.ControlDate';
var controlTime = '#ViewBag.ControlTime';
var source = '#ViewBag.DetailSource';
var page = '#ViewBag.page';
function loadDetailData() {
var param = new Object();
param.BatchDate = controlDate;
param.BatchTime = controlTime;
param.JobGroup = jobGroup;
param.JobName = jobName;
param.DetailTable = detailTable;
param.FilterBatchControl = filterBatchControl;
param.Source = source;
param.page = page;
window.parent.loadingDetailsHeader();
$.ajax({
url: "/control/detailgriddata",
dataType: 'json',
type: 'POST',
data: param,
async: false,
success: function (response) {
try {
jgGridDetailColumnNames = response.JqGridDetailColumnNames;
//jqGridDetailColumnData = response.JqGridDetailData;
jqGridDetailColumnData = response.config;
$('#detailGrid').jqGrid('setGridParam', {colNames: jgGridDetailColumnNames});
$('#detailGrid').jqGrid('setGridParam', {data: jqGridDetailColumnData}).trigger('reloadGrid');
parent.loadingDetailsHeaderComplete();
}
catch(e) {
window.parent.loadingDetailsHeaderException(e.Message);
}
return false;
},
error: function (xhr, ajaxOptions, thrownError) {
alert(xhr.status);
alert(thrownError);
}
});
}
function exportdetails(date) {
var param = new Object();
param.db = source;
param.jobGroup = jobGroup;
param.jobName = jobName;
param.detailTable = detailTable;
param.filterBatchControl = filterBatchControl;
param.filterDate = date;
param.filterTime = "NULL";
$.ajax({
type: 'POST',
contentType: 'application/json; charset=utf-8',
url: '#Url.Action("ExportDetailsCsv", "Control")',
dataType: 'json',
data: $.toJSON(param),
async: false,
success: function (response) {
window.location.assign(response.fileName);
},
error: function (xhr, ajaxOptions, thrownError) {
alert("Details Export Exception: " + xhr.status);
}
});
}
//<![CDATA[
$(document).ready(function () {
'use strict';
$(window).resize(function () {
$("#detailGrid").setGridWidth($(window).width());
}).trigger('resize');
var dgrid = $("#detailGrid");
$('#detailGrid').jqGrid('clearGridData');
loadDetailData();
dgrid.jqGrid({
datatype: 'json',
data: jqGridDetailColumnData,
colNames: jgGridDetailColumnNames,
colModel: [ #Html.Raw(#ViewBag.ColModelDetail) ],
rowNum: 25,
rowList: [25, 50, 100],
pager: '#detailPager',
gridview: true,
autoencode: false,
ignoreCase: true,
viewrecords: true,
altrows: false,
autowidth: true,
shrinkToFit: true,
headertitles: true,
hoverrows: true,
height: 300,
onSelectRow: function (rowId) {
//This is a demo dialog with a jqGrid embedded
//use this as the base for viewing detail data of details
//$('#dialogGrid').dialog();
//gridDialog();
},
loadComplete: function (data) {},
gridComplete: function (data) {
//if (parseInt(data.records,10) < 50) {
$('#detailPager').show();
//} else {
//$('#detailPager').show();
//}
}
}).jqGrid('navGrid', '#detailPager', { edit: false, add: false, del: false, search: false }, {});
});
//]]>
</script>
<table id="detailGrid">
<tr>
<td />
</tr>
</table>
<div id="detailPager"></div>
<div id="dialogGrid"></div>
Probably you should consider to use server side paging instead of returning 10000 rows to the client? Server side paging of SQL data can be implemented much more effectively as client side paging (sorting of large non-indexed data in JavaScript program).
One more option which you have is the usage of another JSON serializer. For example it can be protobuf-net, ServiceStack.Text (see here too), Json.NET and other. In the way you can additionally improve performance of your application comparing with JavaScriptSerializer.

Map JSON data to Knockout observableArray with specific view model type

Is there a way to map a JSON data object to an observable array and then in turn have each item of the observable array be initialized into a specific type of view model?
I've looked at all of knockout's documentation along with the knockout and mapping examples here and I can't find any answer that works for what I'm after.
So, I have the following JSON data:
var data = {
state : {
name : 'SD',
cities : [{
name : 'Sioux Falls',
streets : [{
number : 1
}, {
number : 3
}]
}, {
name : 'Rapid City',
streets : [{
number : 2
}, {
number : 4
}]
}]
}
};
And I have the following view models:
var StateViewModel = function(){
this.name = ko.observable();
this.cities = ko.observableArray([new CityViewModel()]);
}
var CityViewModel = function(){
this.name = ko.observable();
this.streets = ko.observableArray([new StreetViewModel()]);
}
var StreetViewModel = function(){
this.number = ko.observable();
}
Is it possible, with the given data structure and using knockout's mapping plugin, to have the resulting StateViewModel contain an observableArray populated with 2 CityViewModels, and each CityViewModel containing an observableArray populated with 2 StreetViewModels?
Currently using the mapping plugin I'm able to get it to map to a StateViewModel, but the 'cities' and 'streets' collections are populated with generic objects instead of instances of my City and Street view models.
They end up with the correct observable properties and values on them, they're just not instances of my view models, which is what I'm after.
Check this http://jsfiddle.net/pTEbA/268/
Object.prototype.getName = function() {
var funcNameRegex = /function (.{1,})\(/;
var results = (funcNameRegex).exec((this).constructor.toString());
return (results && results.length > 1) ? results[1] : "";
};
function StateViewModel(data){
this.name = ko.observable();
ko.mapping.fromJS(data, mapping, this);
}
function CityViewModel(data) {
this.name = ko.observable();
ko.mapping.fromJS(data, mapping, this);
}
function StreetViewModel(data) {
this.name = ko.observable();
ko.mapping.fromJS(data, mapping, this);
}
var mapping = {
'cities': {
create: function(options) {
return new CityViewModel(options.data);
}
},
'streets': {
create: function(options) {
return new StreetViewModel(options.data);
}
}
}
var data = { state: {name:'SD', cities:[{name:'Sioux Falls',streets:[{number:1},{number:3}]},
{name:'Rapid City',streets:[{number:2},{number:4}]}]}};
var vm = new StateViewModel(data.state)
console.log(vm);
console.log(vm.getName());
console.log(vm.cities());
console.log(vm.cities()[0].getName());
console.log(vm.cities()[0].streets());
console.log(vm.cities()[0].streets()[0].getName());
​