Enabling Dependency Injection for client side models bound to Aurelia enhanced razor views

This is the seventh blog post in a series of posts that covers how I’ve set up an ASP.NET Core MVC project with Aurelia in a way that will allow me to render razor views and make use of MVC features such as model binding and form validation while at the same time using Aurelia to give the UI that SPA touch that modern web applications of today so often require. The goal is a setup where most experienced ASP.NET MVC Developers can go on and develop their views sort of as always, while the designated front end developer (=me) can leverage Aurelia to enhance the user experience with all sorts of client side features. All the code can be found in this Github repo.

In this post I’ll make the third and final follow up of a previous post titled “Using the ASP.NET MVC view model on the client with Aurelia bindings”.

Let’s remind ourselves of the issue with the current client side module used to connect the MVC Model to the client side model used when enhancing the view:

export function create(data: HomePersonModel) {
	return new HomePersonClientModel(data);
}
 
class HomePersonClientModel {
	data: HomePersonModel;
	currentYear: number;
 
	constructor(data: HomePersonModel) {
		this.data = data;
		this.currentYear = new Date().getFullYear();
	}
}
 
interface HomePersonModel {
	firstName: string;
	lastName: string;
	age: number;
}

The issue is underlined with red; Newing up the client model manually like this will prevent us from taking advantage of the Aurelia Dependcy Injection features, which will be a great limitation in our future development.

As the example for this post I’ve decided that this page needs access to the Aurelia Router. The reason for this could for example be some interaction in the page where you’ll need to navigate the user away to some other route of the application. What we need to achieve is a version of the HomePersonClientModel that looks somewhat more like this:

import { autoinject } from 'aurelia-framework';
import { Router } from 'aurelia-router';
 
/.../
 
@autoinject
class HomePersonClientModel {
	data: HomePersonModel;
	currentYear: number;
 
	constructor(data: HomePersonModel, private router: Router) {
		this.data = data;
		this.currentYear = new Date().getFullYear();
	}
}

But this raises another issue, our create function (previous code snippet) will now not compile, because it’s trying to call the constructor. So how can we achieve injecting the data object so that our main goal – share the server side model object to the client – doesn’t get broken?

Before continuing you should be somewhat familiar with the Aurelia DI framework, the basics can be found here in the Aurelia Docs. Another resource that led me to solve this issue was the Pluralsight course Aurelia Fundamentals. Pluralsight is a great resource, so if you don’t already have a subscription I can definitely vouch for its’ value.

The solution involves the use of “named dependencies” and it will come with one sacrifice; The use of @autoinject will have to be replaced with the slightly less maintainable @inject decorator. If you want to follow along, here is the starting point for this blog post.

The first thing we’ll do is to add two new exports to the aurelia-enhancer.ts module, one class and one function:

export class AureliaEnhanceMetaData {     
/**
*
@param dataTypeName The name used to name the instance in Aurelia DI, as a convention use the name of the interface that the data object implements.     
*
@param clientModelType The type that should be created through Aurelia DI and used as client side model for the enhanced markup.
*/
constructor(public dataTypeName: string, public clientModelType: Function) { } } export function createViewModel(metaData: AureliaEnhanceMetaData, data?: any): any { if (data) { aurelia.container.unregister(metaData.dataTypeName); aurelia.use.instance(metaData.dataTypeName, data); } aurelia.use.transient(metaData.clientModelType); let viewModel = aurelia.container.get(metaData.clientModelType); return viewModel; }

First we define the AureliaEnhanceMetaData class. The first argument of the constructor is a string that will be used as a key for the data object we will wanna inject to the client side model, in our case we will use the string ‘HomePersonModel’. The second argument is the client side model type (typed as Function), in our case the “HomePersonClientModel”. An instance of AureliaEnhanceMetaData is what will be newed up inside the home-person.ts module, instead of the current create function (we’ll get to that shortly).

Secondly we define the createViewModel function. This function takes said instance of AureliaEnhanceMetaData, as well as the data object (which is the json generated version of the server side MVC Model class).

Inside the function we check if we have any data (this should be optional as the there might be views where the server side data is not necessary for client side behaviours). If we have data we use the aurelia DI container to unregister any existing dependencies that might already exist under the given name in the *MetaData object, and then we tell Aurelia DI that when anyone wants to resolve a dependency under the given name, it should return the data instance, using aurelia.use.instance(/…/).

After that we tell Aurelia DI that when anyone wants to resolve a dependency of the type stored in metaData.clientModelType it should always create a new instance, using aurelia.use.transient(/…/). The reason for this is that whenever this type is requested, we need to make sure that the named dependency (i.e. the data dependency) will be “re-resolved” to whatever data instance we last associated it with, i.e. the json version of our model object.

Directly after that we instruct Aurelia to actually resolve this instance (container.get) and return it. This is the point where our new constructor in HomePersonClientModel will be called, it will see the Router dependency and resolve it. But how will it resolve the data dependency?

This is were we’ll need to do a few changes to the home-person.ts  module (changes underlined);

import { inject } from 'aurelia-framework'; import { Router } from 'aurelia-router'; import { AureliaEnhanceMetaData } from '../../core/aurelia-enhancer'; let dataKey = 'HomePersonModel'; export function createMetaData(data: HomePersonModel): AureliaEnhanceMetaData { return new AureliaEnhanceMetaData(dataKey, HomePersonClientModel); } @inject(dataKey, Router) class HomePersonClientModel { data: HomePersonModel;
router: Router; currentYear: number; constructor(data: HomePersonModel, router: Router) { this.data = data;
this.router = router; this.currentYear = new Date().getFullYear(); } } interface HomePersonModel { firstName: string; lastName: string; age: number; }

First we’re importing all the stuff we need to import. Then we define a module level string called dataKey, that by convention I just let contain the name of the interface that the data object implements. This key just needs to be something uniqely used for exactly this kind of data object, so it makes sense to use that name. Hopefully our application will not have multiple types named “HomePersonModel”.

After that the create function has been replaced with a createMetaData function. Instead of returning a ready made instance of HomePersonClientModel, it now returns an instance of the previously described AureliaEnhanceMetaData, i.e. the dataKey and the HomePersonClientModel type reference. The name of the class – HomePersonClientModel – matches the Function type, as when compiled to plain javascript any typescript class name is actually a function.

Finally the HomePersonClientModel has been decorated with @inject, and here is where the magic happens; When @inject receives a string instead of a type reference the Aurelia DI  resolver will look for an instance registered under this name, and that’s exactly what we’re doing in the createViewModel function (previous code snippet). This can not be achieved which the @autoinject decorator, so we’ll need to use the @inject decorator, which requires dependencies to be listed in the same order as they are received in the constructor.

The last piece of the puzzle is to update the AureliaEnhanceTagHelper (changes underlined);

output.PostElement.AppendHtml($@"
<script>
	SystemJS.import('app/core/aurelia-enhancer').then(function(enhancer) {{
		SystemJS.import('{Module}').then(function(module) {{
			var data = {jsonData};
			var diMetaData = module.createMetaData();
			var clientModel = enhancer.createViewModel(diMetaData, data);
			enhancer.enhance(clientModel, document.getElementById('{elementId}'));
		}});
	}});
</script>");

In the client side script appended by the TagHelper we’ll now get the meta data object from the module, call the createViewModel function which will now create the model using dependency injection,  and then we’ll pass it along to the enhance function as before.

Running the application at this point will give no visible differences. Everything works as before, but the big difference is that the PersonHomeClientModel now has access to the Aurelia Router and could be extended to make use of any other dependency it might have on Aurelia or any other service you might create in the application. Have a look at the complete diff here.

If you liked this post, please click thumbs up and don’t hesitate to make any comments, questions or other remarks.

Leave a Reply

Your email address will not be published. Required fields are marked *


CAPTCHA Image
Play CAPTCHA Audio
Reload Image