Milos Protic
Posted on March 20, 2019
In the world of all growing popularity of front-end development, it seems that the other side, the backend, has been put to the side. This is due to the super useful libraries and frameworks out there, like Vue, React or Angular.
If you ask me, I disagree with the assumption that the backend is less popular or useful to learn than the front-end development. ASP .NET has a lot of great features released in the previous years, and it open-sourced, which was a huge step for Microsoft I believe.
In this post, I will show a way how to integrate Vue with your ASP .NET MVC application, meaning that our server-side view models will be serialized and represented as our Vue app data.
We can achieve this (on a single MVC view) by creating the following:
- Vue Data - ASP .NET Attribute
- Vue Parser Service
- Home View Model (example page)
- Home Page - Index.cshtml (example page)
- Vue App
Note that I will exclude the need for the page layout (_Layout.cshtml) and base classes since I want to make this post as simple as possible. With the proper architecture, for example, you would have a base view model to hold the data dictionary and the serialization would be done on a layout page or within a helper method.
Vue Data - ASP .NET Attribute
Attributes are a very useful and powerful tool in the ASP .NET world. They can be responsible for a simple task such as an object property validation (is the field required, what is the maximum length allowed...etc.), or they can wrap more complex logic, for example, user authorization.
They can be applied to many different targets, but the most commonly used are:
- Class
- Property
- Method
Our VueData attribute will target only property and will be allowed multiple times on the same class, meaning that we could apply this attribute to as many properties as we want on a specific object.
The attribute will inherit the core Attribute class and it will have only one string property (Name) which will represent the name of the data property within our Vue app.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = true)]
public class VueData : Attribute
{
public VueData(string name)
{
Name = name;
}
public string Name { get; }
}
Vue Parser Service
This service will be responsible for parsing the properties with the VueData attribute applied. It will use reflection to loop through all properties of an object, check if the property has the attribute applied and if it does, it will populate a dictionary that will be serialized to the client later on.
// the interface
public interface IVueParser
{
Dictionary<string, object> ParseData<TModel>(TModel model);
}
// the implementation
public class VueParser : IVueParser
{
public Dictionary<string, object> ParseData<TModel>(TModel model)
{
var props = model.GetType().GetProperties();
var result = new Dictionary<string, object>();
foreach (var prop in props)
{
var attr = prop.GetCustomAttributes(typeof(VueData), true)?.FirstOrDefault()
as VueData;
if (attr == null)
{
continue;
}
result.Add(attr.Name, prop.GetValue(model));
}
return result;
}
}
Ok, now when we have our parsing mechanism implemented, it's time to put it in action by creating an example page and view model.
Home View Model (example page)
Earlier in this post, I wrote that we want to define our server-side view model as a source for the Vue application. By creating the above attribute and service we made this possible.
Let's say that we want to have a header with menu items and a body with a welcome message on the view. As MVC pattern applies, we will create a view model object with two properties. After all, a view model is an object representation of a view. The first property will be a type of string and the second will be a type of string list.
And all we need to do in order to make our view model properties accessible to the serialization (will be explained later in this post) is to apply the attribute to it. Something like the following:
public class HomeViewModel
{
[VueData("message")]
public string Message { get; set; } = "Hello from Vue!";
[VueData("menu")]
public List<string> MenuItems { get; set; } = new List<string>()
{
"Menu 1",
"Menu 2",
};
public string RazorMessage { get; set; } = "Hello from Razor!";
// in a real app, this would be placed in the base view model class
public Dictionary<string, object> VueData { get; set; } = new Dictionary<string, object>();
}
In addition, we will add the property called RazorMessage (string type) just to show that we can combine the two ways of data presentation in our view. This property will not be serialized and accessible to the client.
The view model is ready, time to move on to the next step, which is to create a view.
Home Page - Index.cshtml
In our view, we want to use a server-side view model as a data source and a Vue component to present that data to the user. How can we do this?
First, we need to create the actual MVC controller and a Razor view. This is needed in order to navigate to the page in the first place (we are not using Vue router).
The Controller
public class HomeController : Controller
{
public ActionResult Index()
{
// create our view model and parser
var viewModel = new HomeViewModel();
var parser = new VueParser(); // in the real app you would use DI
// in a real app, this would be placed somewhere in the base controller
viewModel.VueData = parser.ParseData(viewModel);
return View(viewModel);
}
}
The View
@model ..MyNamespace/HomeViewModel
@{
// here we do the serialization of our dictionary into JSON using Newtonsoft.Json package
// this object will be used in our Vue application
// over the years of web development, I've found out that the following
// serialization solves the problems I've encountered so far
// in a real app this would be created as a helper method somewhere
// if we want to exclude the c# code from our view
var serializationSettings = new JsonSerializerSettings()
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
StringEscapeHandling = StringEscapeHandling.EscapeHtml
};
var data =
Html.Raw(
HttpUtility.JavaScriptStringEncode(
JsonConvert.SerializeObject(
Model.VueData, Formatting.None, serializationSettings
),
false)
);
}
<!DOCTYPE html>
<html>
<head>
<title>Vue and .NET synergy</title>
</head>
<body>
// Here we can combine the Razor with Vue without problems
<main id="vue-app">
<header-component v-bind:menu-items="menu"></header-component>
<div>@Model.RazorMessage</div>
<div>{{message}}</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
<script src="...my-path/header-component.js"></script>
// place for the script from the next section here
</body>
</html>
The following script should be placed before the closing of a body tag:
new Vue({
el: '#vue-app',
data: function() {
// parse the serialized data
return JSON.parse('@data');
}
});
The header-component.js source code:
// header-component.js
// in a real app you might use TypeScript or ECMAScript
// in a real app this would be a single file component probably
Vue.component('header-component', {
props: ['menuItems'], // pass in the menu from the app
template: '<header><ul><li v-for="item in menuItems">{{ item }}</li</ul></header>'
});
If you run the page the output should look like this:
- Menu 1
- Menu 2
Hello from Razor!
Hello from Vue!
Conclusion
Note that the same approach can be used to create a connection with the Vue props, only in this case the parsing is a little bit different since Vue props could be defined in several ways.
The solution here would be to create a VuePropertyDescriptor class and use it as a value for each Vue prop. This class would reflect the Vue prop interface. It would contain properties like type, default, required...etc. But this is a topic for another post.
Sometimes going with the full front-end app is not an option. In those moments implementations like the one from this post can come in handy. With the proper architecture and separation of concerns, this could be very powerful and it can increase the speed of development.
For more up to date articles, be sure to check out devinduct.com
Thank you for reading and see you in the next post!
Posted on March 20, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
January 26, 2020