Scaffolding all the client side models under an AngularJS application could be a laborious task. Even more laborious could become if we are to make changes to our model server side, and then have those changes be reflected client side. Now Imagine you are to have a good number of these models under a single application, moreover you are the only developer making this application a reality.
The technique exposed here is not always applicable. Nonetheless, It could be utilized in some application which could end up saving a great degree of time and heartache. In my scenario, the application utilized a NoSQL database on the back end. For most applications with this nature (NoSQL Databases such as RavenDB, MongoDB or DynamoDB) It is easy to implement what I am about to discuss.
AngularJS Client Side Behavior
In order to illustrate how this application behave in an AngularJS upon a request server side, I have created the following Plunker. A emulator Angular service, emulates the request to the server
View Code In Plunker
No need to let AngularJS know your exact server side model
In a big application like the one I have defined and when the scenario is applicable. Thanks to.NET reflection, there is no need to let AngularJS know about the exact model existing server side. We rather let AngularJS know just enough so it would render the view appropriately.
Since this technique would prevent us to tell AngularJS about the exact model, then we could end up with a Controller Fits All (CFA) and A View Fits All (VFA).
The Model Feel.
For a model in a NoQL database with the following model tree.
1: public class Member
2: {
3: // MongoDB.Bson.ObjectId member
4: public ObjectId _id { get; set; }
5: public MemberInformation MemberData { get; set; }
6: }
7: public class MemberInformation
8: {
9: public string Name { get; set; }
10: public string LastName { get; set;
11: public DateTime Member_Since { get; set; }
12: public string Email { get; set; }
13: public int ZipCode { get; set; }
14: }
In the example for this blog post however, I have created the following model.
1: public class MarketValueItem
2: {
3: public MarketValueItem()
4: {
5: id = Guid.NewGuid();
6: MarketValue = new MarketValue();
7: }
8: // would be a Bson object on MongoDB Data Base
9: public Guid id { get; set; }
10: public MarketValue MarketValue { get; set; }
11:
12: }
13: public class MarketValue
14: {
15: [Description("Money Market Fund")]
16: public double MoneyMarketFund { get; set; }
17: [Description("Common Stocks")]
18: public double CommonStocks { get; set; }
19: [Description("Stock Mutual Fund")]
20: public double StockMutualFund { get; set; }
21: [Description("Bond Fund")]
22: public double BondFund { get; set; }
23: [Description("As of")]
24: public DateTime ValueDate { get; set; }
25:
26: }
Angular Model Creator
Thus, we embark in the journey of creating a flexible model. A model fits all that will take information all the field information that we will then bind to Angular. This is the only model that angular would now about.
1: public class FieldInformation
2: {
3: // the name of the field as to be presented to the view
4: public string Label { get; set; }
5: // the intrinsic or real name of the field as to be serializable.
6: public string FieldName { get; set; }
7: // the actual value of the field and as to be serializable
8: public object FieldValue { get; set; }
9: // the input type, as to help Angular learn what type of input to render
10: public string inputType { get; set; }
11: }
Model Factory
Subsequently to querying our data, we move to produce data with the above model properties. .NET reflection is here our great helper.
First We iterate through the properties of any model, that we are to pass to the below static Function
1: public static List<FieldInformation> ExtractFields(object ob)
2: {
3: List<FieldInformation> fields = new List<FieldInformation>();
4: var properties = ob.GetType().GetProperties();
5: foreach (PropertyInfo propinfo in properties)
6: {
7: fields.Add(ExtractFieldInfo(propinfo, ob));
8: }
9: return fields;
10: }
1: private static FieldInformation ExtractFieldInfo(PropertyInfo propinfo, object ob)
2: {
3: FieldInformation info = new FieldInformation
4: { FieldName = propinfo.Name,
5: Label = ExtractFieldDescription(propinfo),
6: FieldValue = ExtractFieldValue(propinfo, ob),
7: inputType = InputTypes[propinfo.PropertyType]
8: };
9: return info;
10: }
1: private static string ExtractFieldDescription(PropertyInfo info)
2: {
3: string description = string.Empty;
4: var attrs = info.GetCustomAttributes(true);
5: var attr = attrs.Where(g => g is DescriptionAttribute)
6: .Select(m => m as DescriptionAttribute).FirstOrDefault();
7: description = attr.Description;
8: return description;
9: }
1: private static object ExtractFieldValue(PropertyInfo info, object ob)
2: {
3: return info.GetValue(ob);
4: }
1: private static Dictionary<Type, string> InputTypes
2: {
3: get
4: { var inputs = new Dictionary<Type, string>();
5: inputs.Add(typeof(double), "number");
6: inputs.Add(typeof(bool), "checkbox");
7: inputs.Add(typeof(DateTime), "date");
8: inputs.Add(typeof(string), "text");
9: inputs.Add(typeof(int), "number");
10: return inputs;
11: }
12: }
Finally, We are Done!
In an MVC we can pass the result of the transformed model
1: private List<VMITEM> Funds
2: {
3: get
4: { string datapath = Path.Combine(Server.MapPath("~/Data").ToString(), "Portfolio.TXT");
5: var q = new QueryData().RetrieveFunds(datapath);
6: var list = q.Select(g => new VMITEM { MapId = g.id.ToString(),
7: // calling the model transformer below
8: fields = Worker.AngModelCreator.Creator.ExtractFields(g.MarketValue) }).ToList<VMITEM>();
9: return list;
10: }
11: }
1: public ActionResult ExtractPortfolio()
2: {
3: // I serialized with JsonConvert only to properly serialize Date Formats
4: return Json(JsonConvert.SerializeObject(Funds), JsonRequestBehavior.AllowGet);
5: }
The resulted JSON string
The following will pass a json string as the below
1: [{
2: "MapId": "af1d7bd8-5378-4b62-89ea-220c2dd22764",
3: "fields": [{
4: "Label": "Money Market Fund",
5: "FieldName": "MoneyMarketFund",
6: "FieldValue": 27000,
7: "inputType": "number"
8: }, {
9: "Label": "Common Stocks",
10: "FieldName": "CommonStocks",
11: "FieldValue": 52000,
12: "inputType": "number"
13: }, {
14: "Label": "Stock Mutual Fund",
15: "FieldName": "StockMutualFund",
16: "FieldValue": 128000,
17: "inputType": "number"
18: }, {
19: "Label": "Bond Fund",
20: "FieldName": "BondFund",
21: "FieldValue": 53000,
22: "inputType": "number"
23: }, {
24: "Label": "As of",
25: "FieldName": "ValueDate",
26: "FieldValue": "2006-12-10T00:00:00",
27: "inputType": "date"
28: }]
29: },
30: .......
AngularJS, The view
Given the above JSON string, we could construct a view in the following manner:
1: <div class="col-sm-9" >
2: <div class="row">
3:
4: <button type="button" class="btn btn-danger" ng-click="back()">
5: << Back
6: </button>
7:
8: <form class="form-horizontal" role="form">
9: <div class="form-group" ng-repeat="Field in Model.fields">
10: <div class="col-sm-3">
11: <label for="inputfield" class="control-label">{{ Field.Label }}</label>
12: </div>
13: <div class="col-sm-9" >
14: <input type="{{Field.inputType}}" class="form-control" id="inputfield" ng-model="Field.FieldValue" placeholder="{{ Field.Label }}" />
15: </div>
16: </div>
17: <div class="form-group">
18: <div class="col-sm-3"></div>
19: <div class="col-sm-9">
20: <div class="checkbox">
21: <label>
22: <button type="button" class="btn btn-success" ng-click="Update(Model)" ng-show="!Loading">Update</button>
23: </label>
24: </div>
25: </div>
26: </div>
27: </form>
28: </div>
29: </div>
AngularJS, The controller
In the code example, I created two types of models / records in order to prove the proper rendering of both elements in a single controller. I created a navigation view that will update the $rootScope this will then specify to Angular what type of view does the user desire to render, a “Funds” view or a “Holder” view.
1: var AngularApp = angular.module("MultipleModelsApp", ['ngRoute']);
2: AngularApp.factory('ViewService', function ($http, $q, $rootScope) {
3: var views = [{
4: name: 'Funds',
5: Title: "Fund History",
6: url: '/vm/ExtractPortfolio',
7: queurl: '/vm/FundByID',
8: quenewurl: '/vm/NewFund',
9: updateurl: '/vm/SubmitFund'
10: }, {
11: name: 'Holders',
12: Title: "Account Holders",
13: url: '/vm/ExtractHolders',
14: queurl: '/vm/HolderById',
15: quenewurl: '/vm/NewHolder',
16: updateurl: '/vm/SubmitHolder'
17: }];
18: return {
19: getViews: function () {
20: return views;
21: },
22: getUrl: function (type) {
23: var query = (type == 'query') ? "$.queurl" : (type == 'new') ? "$.quenewurl" : (type == 'update') ? "$.updateurl" : "$.url";
24: var view = $rootScope.view;
25: var url = Enumerable.From(views)
26: .Where(function (x) {
27: return x.name == view
28: })
29: .Select(query)
30: .FirstOrDefault();
31: return url;
32:
33: },
34: getTitle: function () {
35: var view = $rootScope.view;
36: var title = Enumerable.From(views)
37: .Where(function (x) {
38: return x.name == view
39: })
40: .Select(function (x) {
41: return x.Title
42: })
43: .FirstOrDefault();
44: return title;
45: }
46: }
47: });
1: AngularApp.controller("NavCtrl", function ($scope, $rootScope, ViewService) {
2:
3: $scope.views = ViewService.getViews();
4: $rootScope.views = ViewService.getViews();
5:
6:
7: $scope.init = function () {
8: $rootScope.view = "Funds";
9: }
10: $scope.init();
11:
12: $scope.setView = function (view) {
13: if (view !== $rootScope.view) {
14: $rootScope.view = view;
15: $rootScope.$emit("App.ViewChange");
16: }
17: }
18:
19: });
1: <body ng-app="MultipleModelsApp" class="container" >
2:
3: <div class="col-sm-6" ng-controller="NavCtrl">
4: <div class="col-sm-3" ng-repeat="view in views">
5: <button type="button" class="btn btn-info" ng-click="setView(view.name)" >{{view.name}}</button>
6: </div>
7:
8: </div>
9: <div class="col-sm-12 content" ng-view="">
10:
11: </div>
12:
13: </body>
1: AngularApp.controller("ModifyCtrl", function ($scope, DataRequestPost, $routeParams, $window, $rootScope, ViewService) {
2:
3: $scope.id = $routeParams.id;
4:
5: $scope.Loading = false;
6:
7: $scope.back = function () {
8: $window.history.back();
9: }
10:
11: var retrieveModel = function () {
12: var url = ViewService.getUrl('query');
13: var params = { id: $scope.id };
14: DataRequestPost.get(url, params).then(function (data) {
15: var model = JSON.parse(data);
16: $scope.Model = resolvemodel(model);
17: });
18: }
19:
20: var resolvemodel = function (model) {
21: var fields = model.fields;
22: var length = fields.length;
23: for (var i = 0; i < length; i++) {
24:
25: if (model.fields[i].inputType === "date") {
26: var day = new Date(model.fields[i].FieldValue);
27: model.fields[i].FieldValue = new Date(day.getTime() + day.getTimezoneOffset() * 60000);
28: };
29: }
30: return model;
31: }
32:
33: var UpdateModel = function (model) {
34: $scope.Loading = true;
35: var url = ViewService.getUrl('update')
36: var params = { model: JSON.stringify(model) };
37: DataRequest.get(url, params).then(function (data) {
38: $scope.Loading = false;
39:
40: });
41:
42: }
43:
44: $scope.Update = function (model) {
45: UpdateModel(model);
46: }
47:
48: $scope.init = function () {
49: retrieveModel();
50: }
51: $scope.init();
52:
53: });
Same View / Same Controller
Transform Back to the .NET Server Model - On Update
Since our .NET model suffered a transformation to be rendered in Angular, We need to transform the model to its .NET representation.
1: public static object TransformFields(List<FieldInformation> fields, object context)
2: {
3: foreach (FieldInformation FieldInfo in fields)
4: {
5: context.GetType().GetProperty(FieldInfo.FieldName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance).SetValue(context, FieldInfo.FieldValue);
6: }
7: return context;
8: }
Then on Submit of the below model to the server, we will have the following server side behavior