Using the ASP.NET MVC view model on the client with Aurelia bindings

This is the fourth 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 share the first step of the setup that was of one of my main goals with this approach of joining together asp.net mvc and aurelia; Sharing the same data model across server and client side development.

The spec for what we’ll be building could be expressed as follows;

  • A form is shown with the fields First name, Last Name, Occupation and Age.
  • When First name and Last name are edited, the Full name is immediately displayed in the form.
  • When Age is edited, the year of birth is immediately displayed in the form.

I’ve already taken some steps in a few preparing commits;

  • Added the Newtonsoft.Json nuget package and some classes and helper functions for setting up the serialization of .net objects into json. Notably the serialization settings turns the PascalCased properties into camelCased.
  • Created a view, accessible on route /Home/Person/, populated the view with a PersonModel model object and prepared a client side model with single currentYear property populated in the constructor.

Each of the input fields are marked up following this pattern:

<div class=”form-group”>
    <label asp-for=”FirstName“></label>
    <input asp-for=”FirstName class=”form-control” value.bind=”firstName” />
</div>

For the Occupation field, there is no value.bind attribute. The surrounding div has a th-aurelia-enhance-module tag helper attribute that will enhance the markup and associate it with the /app/views/home/home-person.ts client side module.

The model object is populated and returned from the controller like this:

public IActionResult Person()
{
    PersonModel model = new PersonModel
    {
        FirstName = “John”,
        LastName = “Doe”,
        Occupation = “Developer”,
        Age = 35
    };
    return View(model);
}

Running the view at this point will present a strange issue, all fields except the Occupation field shows up as empty! The html sent from the server will contain the values, as they are model bound by mvc. But when the markup is enhanced, aurelia will actually look for the firstName, lastName and age properties in the client side model, and since they are not there the value attributes will be emptied.

However, when editing the name and age fields, the full name and year of birth output are updated as expected, so what actually happens is that Aurelia adds these properties to the client model.

Let’s fix this to get the expected behaviour. If you want to follow along, here is the starting point. The first step is an update to the AureliaEnhanceTagHelper:

private const string AuDataAttributeName = “th-aurelia-enhance-data”;
[HtmlAttributeName(AuDataAttributeName)]
public object Data { get; set; }

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    /…/

    if(!String.IsNullOrEmpty(Module))
    {
        string jsonData = “{}”;
        if (Data != null)
        {
            jsonData = Data.ToJsonCamelCase();
        }
        output.PostElement.AppendHtml($@”
            <script>
                SystemJS.import(‘app/core/aurelia-enhancer’).then(enhancer => {{
                    SystemJS.import(‘{Module}‘).then(module => {{
                        var data = {jsonData};
                        var clientModel = module.create(data);
                        enhancer.enhance(clientModel, document.getElementById(‘{elementId}‘));
                    }});
                }});
            </script>”);
    }
    else
    {
        /…/
    }
}

First we are adding another attribute – th-aurelia-enhance-data – associated with the object property “Data”. In the Process method, if Data is set, we serialize it to json and in the script block we send that json object to the create function of the client side module;

export function create(data: any) {
    return new HomePersonClientModel(data);
}

class HomePersonClientModel {
    data: any;
    currentYear: number;

    constructor(data: any) {
        this.data = data;
        this.currentYear = new Date().getFullYear();
    }
}

To separate the client side model and the data model I’m introducing a convention here. The data object that is shared between server and client side code will on the client side model be placed as a property called “data”. This means our value.bind attributes in the razor view needs to be changed from value.bind=”firstName” to value.bind=”data.firstName”.

The last piece of the puzzle is to add th-aurelia-enhance-data=”@Model” to the root div of the Person.cshtml view.

Running the app now will give us the expected result, and the behaviours mentioned in the spec are all achieved by simple aurelia bindings. Here’s the diff for what we did.

As a small cleanup before finishing off this part, I’ve made an additional commit that will make the data property of the HomePersonClientModel typed, by creating an interface mirroring the properties needed from the serverside PersonModel class. The Occupation property is not included there since it doesn’t take part in any client side interactions. This means we don’t really need it when the PersonModel object is serialized so I’ve opted it out from the json serialization using an attribute from the Newtonsoft.Json nuget package. You can see the diff for this commit here.

To summarize the achievement of this step I’d like to point out that what I’ve accomplished here is a really convenient way of working with the data in my application across server and client side code.
   Using Json.net for the serialization also makes it easy to differentiate the client side data object from the server side when needed as chances are the server side model often will contain a lot more than what’s really needed for the client side behaviours. The class level attribute [JsonObject(MemberSerialization.OptIn)] can be convenient when you only need a few properties from the server side class available on the client side. In that case you just add [JsonProperty] on all the properties you need instead of adding [JsonIgnore] to the ones you don’t need.
   Putting the data in a separate property in the client side model is essential for being able to making it strongly typed, but it also gives a nice separation of concern in the client side code as any behaviours (functions) and any support data, such as booleans for toggling css classes, lives directly in the client side model and does not get mixed up with the domain object data.
   Any client side behaviour affecting the data can be aurelia bound to the input fields rendered through asp.net mvc:s asp-for tag helper which means that when this form is eventually posted, they’ll take part in mvc:s modelbinding process for sending the values to the server. This is something I’ll cover in a future post as we need a little setup to ensure any forms are posted through ajax, so that we don’t loose our Single Page Application feeling.

Another thing I’ll rework in a future post will enable value.bind=”data.firstName” to instead be written something like value.bind=”FirstName”, where FirstName actually will be recognized by Visual Studio as a reference to the FirstName property of the PersonModel class in the same way as the built in asp-for tag helper works.

Another issue with the current setup is that the HomePersonClientModel is manually newed in the create function. This will be a problem once we encounter the need to make use of Aurelias dependency injection to access both Aurelia built in singletons as well as our own. I’ll be sure to cover a solution also to this in a future post, so stay tuned!

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