Stu Ratcliffe

Integrating Vue.js with ASP.NET Core MVC


In my last post I wrote about integrating a full Vue.js SPA with an ASP.NET Core backend, including server-side rendering. But what if we don't need a full SPA, and just want to use Vue.js as a jQuery replacement on a page-by-page basis in a standard MVC application?

Application Structure

I'm a big fan of the feature folder application structure. It's been written about enough before that I'm not going to go through it here, but the sample application that goes with this post uses this structure, so it's worth familiarising yourself with it if it's not something you've seen before!

The structure of our application will look something like this:

Features
--HomePage
----Controller.cs
----Index.cshtml
--TodosPage
----Controller.cs
----Index.cshtml
----todos.js
--ContactPage
----Controller.cs
----Index.cshtml
----contact.js

It's not necessary to split every page of an application into a feature folder, but it demonstrates the idea for the purposes of this article. Our Vue.js code will live within the javascript files within these feature folders. However, these folders aren't accessible from our Views. To solve this problem we need a way getting these script files into the wwwroot folder at runtime.

Bundling & Minification

The easiest way to achieve what we want is to use the built in bundling and minification tools that are provided with ASP.NET Core. I've already blogged about these before, so again I won't go into detail here, but our bundleconfig.json file will need to look like this:

[
  {
    "outputFileName": "wwwroot/css/site.min.css",
    "inputFiles": [
      "wwwroot/css/site.css"
    ]
  },
  {
    "outputFileName": "wwwroot/js/site.min.js",
    "inputFiles": [
      "Features/**/*.js"
    ],
    "minify": {
      "enabled": true,
      "renameLocals": true
    },
    "sourceMap": true
  }
]

The important part is the "inputFiles" block of the javascript section. We use wildcards to scan through all of our feature folders and look for any javascript files and include them in the bundle. It's worth mentioning that although this seems to work just fine, when running dotnet bundle watch at the command line, you'll keep seeing the following statement:

No files matched the pattern Features/**/*.js

I'm not sure why this is the case, as it clearly does find the scripts as the generated bundle has everything it needs included.

With this file in place we can drop into a command prompt at the root of our project and run dotnet bundle watch to keep our bundle up to date as soon as we save changes to any of the script files within the feature folders.

Gulp

The bundling and minification tool works just fine, but if you need more control, or different configurations for different environments, you may need to upgrade this setup to use gulp tasks. This is beyond the scope of this article, but there are plenty of examples of configuring gulp tasks for a whole host of use cases, including on Microsoft's own ASP.NET Core documentation

Configuring Vue.js

As we're integrating Vue.js into an MVC application this time, the configuration and setup is a lot simpler. All we need to do is include script references to the relevant CDN locations somewhere within our Views. In the sample application I've used Vue on multiple pages, so it made sense to include the following CDN references in the Features/Shared/_Layout.cshtml file. Make sure these are included before any of our own scripts to make sure they are available when required:

<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vee-validate"></script>

The vee-validate reference is a validation plugin for Vue that we'll use on a sample contact page with client-side validation.

Creating a module

It's good practice to try and keep our scripts organised and modular. To achieve this we'll use self-executing anonymous functions. We can create a simple contact form with client-side validation using the following module:

(function (Vue, VeeValidate) {

  if (document.querySelector('#contact')) {
    Vue.use(VeeValidate);

    var app = new Vue({
      el: "#contact",
      data: {
        name: '',
        email: '',
        message: '',
        errorMessage: ''
      },
      methods: {
        send: function () {
          this.$validator.validateAll().then(result => { 
            if (result) {
              this.reset();
              alert('Form submitted!');
            } else {
              this.errorMessage = 'Please fix all validation errors.'
            }
          });
        },
        reset: function () {
          this.name = '';
          this.email = '';
          this.message = '';
          this.errorMessage = '';
          this.$validator.clean();
        }
      }
    });
  }

})(Vue, VeeValidate);

It should be fairly self explanatory what this script is doing. However, notice we have an if statement surrounding the entire module. We setup our bundleconfig.json file to load all feature folder scripts into a single bundle which is included in our application layout file. This means that if we add multiple modules instantiating multiple Vue instances, they will run on every page of our application. Wrapping our modules with an if statement in this way prevents unwanted Vue instances from running on the pages they are not meant for.

In a large application we may not want to go about things the same way, and would probably prefer a more dynamic bundling approach to only include the relevant scripts on a page-by-page basis. However, this method is just fine for a small example like this.

The ASP.NET Core view that goes with the contact page bundle above looks like this:

@{
    ViewData["Title"] = "Contact";
}
<h2>@ViewData["Title"].</h2>

<div id="contact">
  <form v-on:submit.prevent="send">
    <div v-if="errorMessage.length" class="alert alert-danger">
      {{ errorMessage }}
    </div>
    <div :class="{ 'form-group': true, 'has-error': errors.has('name'), 'has-success': name.length && !errors.has('name') }">
      <label for="name">Name</label>
      <input autofocus v-model="name" v-validate="'required|min:5'" class="form-control" name="name" type="text" />
      <span v-show="errors.has('name')" class="text-danger">{{ errors.first('name') }}</span>
    </div>
    <div :class="{ 'form-group': true, 'has-danger': errors.has('email'), 'has-success': email.length && !errors.has('email') }">
      <label for="email">E-mail</label>
      <input v-model="email" v-validate="'required|email'" class="form-control" name="email" type="text" />
      <span v-show="errors.has('email')" class="text-danger">{{ errors.first('email') }}</span>
    </div>
    <div :class="{ 'form-group': true, 'has-danger': errors.has('message'), 'has-success': message.length && !errors.has('message') }">
      <label for="message">Message</label>
      <textarea v-model="message" v-validate="'required|min:5'" class="form-control" name="message"></textarea>
      <span v-show="errors.has('message')" class="text-danger">{{ errors.first('message') }}</span>
    </div>
    <input type="submit" value="Save" class="btn btn-primary" />
  </form>
</div>

Again, this is fairly self-explanatory, but if this doesn't make sense to you then please have a good read around the documentation for Vue.js and VeeValidate respectively.

That's all there is to it to setup a contact form with reactive client-side validation. From here it's trivial to configure a HTTP module such as axios to AJAX our form data to a controller action and send an e-mail.

A more complex example

Our contact page is a fairly simple example. Most pages in a reasonably complex MVC application will contain some data model from a database or other persistent data store, so how do we get this data into our Vue.js instance?

We're going to create a very simplistic implementation of the standard javascript framework tutorial example - Todo MVC! To keep things simple we are simply using a list of string values for our data model. In a standard MVC page we would have something like this in our controller action:

[Route("todos")]
public IActionResult Index()
{
    var todos = new List<string> 
    {
      "Learn Vue.js",
      "Learn ASP.NET Core"
    };

    return View(new Model { Todos = todos });
}

However, as we need to get this data model into a javascript object, we'll actually serialize the list into a string. We will configure the View to expect the following model to keep things reasonably realistic:

namespace DotnetVueMvc.Features.TodosPage
{
  public class Model
  {
    public string Todos { get; set; }
  }
}

Our controller action then needs to serialize our list of todos so they can be set on the model and passed to the view:

[Route("todos")]
public IActionResult Index()
{
    var todos = new List<string> 
    {
      "Learn Vue.js",
      "Learn ASP.NET Core"
    };

    return View(new Model { Todos = JsonConvert.SerializeObject(todos) });
}

Now that our View has access to the data model, we have two options for getting this data into a Vue instance:

  1. Set the serialized todos string as the value of a hidden input field
  2. Add an inline script tag to the page that assigns the serialized todos string as a variable directly on the window object.

With either of these methods, we need to make use of the @Html.Raw() razor helper to make sure HTML encoding does not interfere with the string being parsed as JSON.

The hidden input option would look something like this:

<input id="todos" type="hidden" value="@Html.Raw(Model.Todos)" />

From here, we can access the value using plain old javascript:

var todos = document.getElementById('todos').value;

It's then a simple case of using this module scoped variable within the data object of a Vue instance:

var app = new Vue({
  el: "#todos",
  data: {
    todos: todos
  }
});

Alternatively, we can inline a script tag to assign the todos directly onto the window object:

<script>
  window.todos = @Html.Raw(Model.Todos);
</script>

And from here it's still a simple case of using this variable directly within the data object of a Vue instance:

var app = new Vue({
  el: "#todos",
  data: {
    todos: window.todos
  }
});

Now we have access to our server-side data model, we can configure our Vue module as normal:

(function () {

  if (document.querySelector('#todos')) {

    var todos = document.getElementById('todos').value;

    var app = new Vue({
      el: "#todos",
      data: {
        todos: window.todos,
        newTodo: ''
      },
      methods: {
        addTodo: function () {
          if (this.newTodo.length) {
            this.todos.push(this.newTodo);
            this.newTodo = '';
          }
        },
        removeTodo: function (index) {
          this.todos.splice(index, 1);
        },
        save: function () {
          alert('Saving: ' + JSON.stringify(this.todos));
        }
      }
    });
  }

})();

The associated ASP.NET Core View file is as follows:

@model DotnetVueMvc.Features.TodosPage.Model
@{
    ViewData["Title"] = "Todos";
}
<h2>@ViewData["Title"].</h2>

<div id="todos">
  <input v-model="newTodo" class="form-control" placeholder="New todo...press enter to store..." v-on:keydown.enter="addTodo" />

  <ul class="todos-list">
    <li v-for="todo, index in todos" v-on:click="removeTodo(index)">{{ todo }}</li>
  </ul>

  <button class="btn btn-primary" v-on:click="save">Save</button>
</div>

<input id="todos" type="hidden" value="@Html.Raw(Model.Todos)" />

<script>
  window.todos = @Html.Raw(Model.Todos);
</script>

This page renders our initial server-side todo list, and then allows us to add additional todos by typing in the input box and pressing enter, or removing a todo by clicking on it in the list. The save action could again easily be configured to use axios to AJAX our updated todo list to a controller action for persistence into a database.

Finally, it's worth mentioning that using this method is exposing your server-side data model directly in the source code for the page. If you aren't happy with this, you'd have to configure an AJAX call to fire as the Vue component is rendered into the DOM, and fetch the data model after the page has loaded. There are obvious performance hits with this method, but these can be made more bearable by setting up loading spinners and such to improve the users experience.

Conclusion

We looked at a simplistic way to keep our client-side script files organised and modular. We then looked at implementing a simple contact page with reactive client-side validation using the excellent VeeValidate plugin for Vue.js. Finally, we increased the complexity a little by integrating a server-side data model with a Vue instance, and creating a simplistic Todo MVC example.

The source code for this post can be found at my github profile.

Discussion

  • Andrei
    Andrei
    06 October 2017

    So instead of using mvc standard and simply rendering and binding the model server side (as MVC is intended to), now we transform the model into a json and serialize it server-side into a hidden field, then on client side we write more js code to deserialize that model and bind it into a vue component. And, of course we also lose the standard MVC form validation as now we use a Vue library to validate which of course will not sync anymore with the mvc server-side validation, so if a field does not validate server-side you're basically f..ked as you'll have to write a lot of pretty ugly code (both server and client side) to show the errors properly the same way you used to do it simply with standard MVC. And this was the simplest example possible and presented as "how to".. Congrats, you have added a ton of complexity and overhead to your project, just for the luxury of having VueJS integrated in. Perfect recipe for a messy project. Sorry to say, but VueJS does not play well with standard forms. You're much better off sticking one way or another, meaning either VueJS (with xhr) and some rest webapi or standard MVC forms without VueJs. Just my opinion, I might be wrong.

  • Ewald
    Ewald
    26 October 2017

    I agree with @Andrei here. Seem like a horrible idea to mix. Stick with either Vue.js or MVC

  • Oems
    Oems
    02 November 2017

    Totally in agreement with ANDREI.

  • Stu Ratcliffe
    Stu Ratcliffe
    03 November 2017

    Whether you choose to take this kind of approach can only be decided by you, based on the requirements of the project and the expectations of those who will be using your app. It is very rare these days that I have the luxury of building a plain and simple MVC application without the need for a lot of javascript. The obvious option here is jQuery, and for small amounts of client side code that is definitely what I would choose. However, in my opinion it is hard to dispute that a) it is easier and b) produces much cleaner / easier to understand code to use Vue instead. As for the MVC validation concerns, if your validation requirements are simple enough to stick with data annotations then that's absolutely fine. However, I quite often end up moving away from data annotations and going with FluentValidation instead. Last I checked, there was no client-side validation support with the latest version of the library for ASP.NET Core. Even if you use data annotations, the client side support isn't the best, and certainly can't compare with a dedicated client-side form validation library. At the end of the day when building SPAs I end up writing separate client/server side validation rules as I haven't found a good way of sharing them, so it's not the end of the world to me to have separate rules in this instance as well - and is it really so difficult to display server rendered validation errors? I don't think so! Ultimately there are no perfect solutions to a software problem. We're in 2017 where SPAs are almost the defacto choice in building a web app, and as such rich client side apps are expected for most of the projects I work on. Yes the examples in this post are simple, and you may not think it necessary to use anything but jQuery, but these are simple examples for the sake of a blog post. Real world components can and usually are much more complicated, at which point the overhead becomes far more acceptable for cleaner, easier to read, and more maintainable javascript code.

  • Chad Carter
    Chad Carter
    11 December 2017

    I appreciate your posts on VueJS and .NET Core. Thanks for both of them. It seems to me that ASP.NET Core Razor Pages is a nice way to handle the Feature folders. My current project is using Razor Pages mostly, with my controllers really just being for the api calls. So I'm not doing a full SPA system, although part of the admin will be a SPA. Anyway, seeing these examples is really beneficial. Just wanted to say thanks for taking the time to create these posts. I'm removing the default dependencies of jQuery validation and replacing it with vee-validate, so this was really beneficial.

  • Marcelo
    Marcelo
    21 February 2018

    Nice implementation How change gulp for webpack? And vuex ?

  • Jaap Taal
    Jaap Taal
    02 March 2018

    @Json.Serialize() is now a thing...