Profile Picture

Sirwan Afifi

Stories from a web developer.

© 2018. Sirwan Afifi All rights reserved.

Uploading file in Vue.js

In this blog post I am going to show you how you can upload file using Vue.js. On the backend we will use ASP.NET Core MVC to expose an endpoint to the client to receive file. Let’s say we have a HTML form for saving a coffee, this form contains a file input to accept coffee’s image:

Vue Upload File

To allow the user to pick a file we need to have a reference to the file input field using ref attribute then we can easily access this input using $refs object inside our Vue instance this is like selecting an element using jQuery:

<input required type="file" ref="image">

As you can see we named this input image so we can access the element this way:

this.$refs.image

Before we can submit the form we can encapsulate functionality inside a service:

import axios from 'axios';

class CoffeeService {

    saveCoffee(coffee: any, file: any): any {
        const formData = new FormData();
        for (const key in coffee) {
            if (coffee.hasOwnProperty(key)) {
                formData.append(key, coffee[key]);
            }
        }
        formData.append(file.name, file);

        return axios.post('/Coffees/SaveCoffee', formData);
    }
}

// Export a singletone instance in the global namespace
export const coffeeService = new CoffeeService();

This service has a method called saveCoffee which accepts two parameters one is the coffee itself and the second one is uploaded file then we are using FormData to combine these data together and sending it to the server using axios library. Keep in mind, this method returns a promise which means that the caller can use then syntax to get server’s response. So inside the CoffeeComponent we can use Coffeeservice like this:

<template>
    <div class="container">
        <h3>Add a new Coffee</h3>
        <form v-on:submit.prevent="submitted">
            <div class="form-group">
                <label class="control-label">Coffee Name</label>
                <input required type="text" class="form-control" name="Name" v-model="model.name">
            </div>

            <div class="form-group">
                <label class="control-label">Coffee Type</label>
                <select class="form-control" name="CoffeeType" v-model="model.coffeeType">
                    <option v-for="coffeeType in coffeeTypes" :value="coffeeType.value" v-bind:key="coffeeType.value"></option>
                </select>
            </div>

            <div class="form-group">
                <label class="control-label">Coffee Image</label>
                <input required type="file" multiple class="form-control" name="Image" @change="fileChange" ref="image">
            </div>

            <button class="btn btn-primary" type="submit">Save</button>
        </form>
    </div>
</template>

<script lang="ts">
import Vue from "vue";
import { Component, Prop, Watch } from "vue-property-decorator";
import { coffeeService } from "../services/CoffeeService";
/// <reference path="./toaster.d.ts" />

@Component({
  components: { UploadFileSimpleComponent }
})
export default class UploadFileSimpleComponent extends Vue {
  coffeeTypes = [
    { name: "Espresso", value: 0 },
    { name: "Latte", value: 1 },
    { name: "Mocha", value: 2 }
  ];

  model = {};

  submitted() {
    coffeeService
      .saveCoffee(this.model, (<any>this.$refs.image).files)
      .then(function(response: any) {
        toastr.success(response.data);
      })
      .catch(function(error: any) {
        toastr.error(error);
      });
  }
}
</script>

Showing upload progress

Now we want to show upload progress, axios makes this easy all we need to do is add a third argument to the post method, this argument is actually an object, inside this object we can configure the request inside this object we have access to an event handler called onUploadProgress which we can do the calculation. let’s change the CoffeeService to this:

import axios from 'axios';
import { eventBus } from '../main';

class CoffeeService {

    saveCoffee(coffee: any, file: any): any {
        const formData = new FormData();
        for (const key in coffee) {
            if (coffee.hasOwnProperty(key)) {
                formData.append(key, coffee[key]);
            }
        }

        formData.append(file.name, file);

        let startTime = Date.now();

        return axios.post('/Coffees/SaveCoffee', formData, {
            onUploadProgress: uploadEvent => {
                const queueProgress = Math.round(uploadEvent.loaded / uploadEvent.total * 100);
                const timeElapsed = Date.now() - startTime;
                const uploadSpeedFirst = uploadEvent.loaded / (timeElapsed / 1000);
                const uploadTimeRemaining = Math.ceil(
                    (uploadEvent.total - uploadEvent.loaded) / uploadSpeedFirst
                  );
                const uploadTimeElapsed = Math.ceil(timeElapsed / 1000);
                const uploadSpeed = uploadSpeedFirst / 1024 / 1024;

                eventBus.$emit('uploadData', {
                    queueProgress,
                    uploadTimeRemaining,
                    uploadTimeElapsed,
                    uploadSpeed
                });
            }
        });
    }
}

// Export a singletone instance in the global namespace
export const coffeeService = new CoffeeService();

Inside the method we construct an object, this object will notify all subscribers with current progress. This notification is done using $emit method. We also need to update the template to show this progress:

<div v-if="uploadDetails.queueProgress > 0">
    <table class="table">
        <thead>
        <tr>
            <th width="15%">Event</th>
            <th>Status</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td><strong>Elapsed time</strong></td>
            <td nowrap> second(s)</td>
        </tr>
        <tr>
            <td><strong>Remaining time</strong></td>
            <td nowrap> second(s)</td>
        </tr>
        <tr>
            <td><strong>Upload speed</strong></td>
            <td nowrap> MB/s</td>
        </tr>
        <tr>
            <td><strong>Queue progress</strong></td>
            <td>
            <div class="progress-bar progress-bar-info progress-bar-striped" role="progressbar"
                aria-valuemin="0" aria-valuemax="100" :aria-valuenow="uploadDetails.queueProgress"
                :style="{ 'width': uploadDetails.queueProgress + '%' }">
                %
            </div>
            </td>
        </tr>
        </tbody>
    </table>
</div>

The backend is a simple MVC controller, all it does is uploading the file and saving the coffee into the database:

public async Task<IActionResult> SaveCoffee(Coffee coffee)
{
    // Uploading files
    var fileName = await UploadFiles();

    // Saving data
    coffee.Image = fileName;
    _coffeeService.Add(coffee);
    _coffeeService.SaveChanges();

    return Json("Coffee has been saved!");
}

private async Task<string> UploadFiles()
{
    var uploadsRootFolder = Path.Combine(_environment.WebRootPath, "uploads");
    if (!Directory.Exists(uploadsRootFolder))
    {
        Directory.CreateDirectory(uploadsRootFolder);
    }

    var files = Request.Form.Files;
    foreach (var file in files)

    {
        if (file == null || file.Length == 0)
        {
            continue;
        }

        var filePath = Path.Combine(uploadsRootFolder, file.FileName);
        using (var fileStream = new FileStream(filePath, FileMode.Create))
        {
            await file.CopyToAsync(fileStream).ConfigureAwait(false);
            return file.FileName;
        }
    }

    return string.Empty;
}

Upload file using Vue.js

You can grab the working sample project from GitHub.

TypeScript and Vue.js

Webpack

You might be wondering why do we need to worry about yet another framework when we already know there are things like Angular, React, … the answer is simplicity. I have used Angular in some of my previous projects by Angular I mean the first version of it (AngularJS 1.x) but these days I feel like Angular team is going to force developers to migrate to Angular (2, 3, 4, 5, 6, …). I really like Vue.js, it’s really a great one because I think Vue.js is Declarative, Easy to Maintain and Powerful. Also, the integration between Vue and TypeScript is really good. Just like other frameworks Vue also has CLI which helps you to scaffold your project quickly. In this blog post, I would like to show how to combine Vue.js with TypeScript inside an ASP.NET Core 2.x application.

Project Setup

The easiest way to get started is by using dotnet cli tool to create a project:

dotnet new mvc --name aspnet-vue-typescript

Then cd to that directory and then use code . to open VSCode. At this point the project structure looks like this:

Sample app

To get started with our client side code we need to install some packages, for doing so we need package.json file:

{
  "name": "aspnet-vue-typescript",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "Sirwan Afifi",
  "license": "ISC",
  "dependencies": {
    "ts-loader": "^4.3.0",
    "typescript": "^2.8.3",
    "vue": "^2.5.16",
    "vue-class-component": "^6.2.0",
    "vue-property-decorator": "^6.1.0",
    "webpack": "^4.9.1",
    "webpack-dev-server": "^3.1.4"
  },
  "devDependencies": {
    "aspnet-webpack": "^2.0.3",
    "css-loader": "^0.28.11",
    "file-loader": "^1.1.11",
    "sass-loader": "^7.0.1",
    "style-loader": "^0.21.0",
    "vue-loader": "^15.2.1",
    "vue-template-compiler": "^2.5.16",
    "webpack-cli": "^2.1.4",
    "webpack-hot-middleware": "^2.22.2"
  }
}

Once you installed this packages using npm install, we’ll create our webpack.config.js file:

let webpack = require('webpack');
const VueLoaderPlugin = require('vue-loader/lib/plugin');
let path = require('path');

module.exports = {
    entry: {
        main: './ClientApp/main'
    },
    output: {
        path: path.resolve(__dirname, 'wwwroot', 'js'),
        filename: '[name].js',
        publicPath: '/js/'
    },
    module: {
        rules: [
            { test: /\.vue$/, loader: "vue-loader" },
            {
                test: /\.tsx?$/,
                loader: 'ts-loader',
                exclude: /node_modules/,
                options: { appendTsSuffixTo: [/\.vue$/] }
            }
        ]
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js'],
        alias: {
            'vue$': 'vue/dist/vue.esm.js' // 'vue/dist/vue.common.js' for webpack 1
        }
    },
    optimization: {
        splitChunks: {
            cacheGroups: {
                commons: {
                    test: /[\\/]node_modules[\\/]/,
                    name: "main",
                    chunks: "all"
                }
            }
        }
    },
    plugins: [
        new VueLoaderPlugin()
    ]
};

As you can see we’re telling webpack where the entry point is, so create a new directory inside the project called ClientApp, this directory contains all Vue’s related files, Inside this directory, create a ts file called main.ts this file is going to be our entry point:

import Vue from 'vue';
import MyComponent from './components/MyComponent.vue';


Vue.config.productionTip = false;

const v = new Vue({
    el: '#app',
    data() {
        return {
            name: 'Sirwan'
        }
    },
    components: {
        MyComponent
    }
});

Next, we have specified the output which is bundle.js file, this file doesn’t yet exist, it’s going to be created by webpack. Then we have specified TypeScript loader for webpack. Next thing left to do is adding tsconfig.json file to the project:

{
    "compilerOptions": {
        "sourceMap": true,
        "noImplicitReturns": true,
        "noImplicitAny": true,
        "experimentalDecorators": true,
        "emitDecoratorMetadata": true,
        "target": "es5",
        "strict": true,
        "module": "es2015",
        "moduleResolution": "node",
        "lib": [
            "es2016",
            "dom"
        ]
    },
    "exclude": [
        "node_modules",
        "wwwroot"
    ]
}

At this point if you run npx webpack you’ll see the bundle file:

Webpack

Now we can add a script reference inside Views/Shared/_Layout.cshtml to bundle.js file:

<script src="~/js/bundle.js" asp-append-version="true"></script>

Now we can use Vue inside our views:

@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
  <h1 class="display-4">Hello, </h1>
</div>

Hot Module Replacement

Webpack has something called Hot Module Replacement:

Hot Module Replacement (HMR) exchanges, adds, or removes modules while an application is running, without a full reload. This can significantly speed up development in a few ways: Retain application state which is lost during a full reload. Save valuable development time by only updating what’s changed. Tweak styling faster – almost comparable to changing styles in the browser’s debugger.

For adding this functionality we need to install webpack-hot-middleware package:

npm i -D webpack-hot-middleware

Then we need to register this component into MVC’s HTTP request pipeline in the Configure method, for doing this we need to install Microsoft.AspNetCore.SpaServices:

app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
    HotModuleReplacement = true
});

This middleware looks for webpack file and automatically executes it for us when we change client side code:

Webpack

Developing Console-based UI in C#

As you know Console Applications don’t have graphical user interface or GUI. Instead, they run from Command Line, for example instead of writing a name into a textbox and clicking a button we would instead invoke the Console Application and provide the name as a parameter. In fact, in Console Applications we instead interact with the user using text inputs. For example, we could prompt something like this:

The user can then write a name and hit enter then we can read that text input into our application.

Wouldn’t be better to make it beautiful? This is where we can use Terminal.Gui a cross-platform GUI toolkit:

Sample app

Terminal.Gui is a library intended to create console-based applications using C#. The framework has been designed to make it easy to write applications that will work on monochrome terminals, as well as modern color terminals with mouse support. This library works across Windows, Linux and MacOS.

Using this UI toolkit is pretty simple, all you need to do is adding its package into your project:

dotnet add package Terminal.Gui

The simplest application looks like this:

using System;

namespace consoleTest
{
    using Terminal.Gui;

    class Demo {
        static int Main ()
        {
            Application.Init ();

            var n = MessageBox.Query (50, 7,
                "Question", "Do you like console apps?", "Yes", "No");

            return n;
        }
    }
}

This example shows a prompt and returns an integer value depending on which value was selected by the user (Yes, No, or if they use chose not to make a decision and instead pressed the ESC key):

Sample app

As you can see the first thing to do is calling Application.Init (); to actually initialize the application. We can also create a window and then add a menu to it:

Application.Init ();
var top = Application.Top;

// Creates the top-level window to show
var win = new Window (new Rect (0, 1, top.Frame.Width, top.Frame.Height-1), "MyApp");
top.Add (win);

// Creates a menubar, the item "New" has a help menu.
var menu = new MenuBar (new MenuBarItem [] {
    new MenuBarItem ("_File", new MenuItem [] {
        new MenuItem ("_New", "Creates new file", ()=> {}),
        new MenuItem ("_Close", "", () => {}),
        new MenuItem ("_Quit", "", () => { top.Running = false; })
    }),
    new MenuBarItem ("_Edit", new MenuItem [] {
        new MenuItem ("_Copy", "", null),
        new MenuItem ("C_ut", "", null),
        new MenuItem ("_Paste", "", null)
    })
});
top.Add (menu);

// Add some controls
win.Add (
    new Label (3, 2, "Login: "),
    new TextField (14, 2, 40, ""),
    new Label (3, 4, "Password: "),
    new TextField (14, 4, 40, "") {  },
    new CheckBox (3, 6, "Remember me"),
    new RadioGroup (3, 8, new [] { "_Personal", "_Company" }),
    new Button (3, 14, "Ok"),
    new Button (10, 14, "Cancel"),
    new Label (3, 18, "Press ESC and 9 to activate the menubar"));

Application.Run ();

Sample app

You can build even complex console-based UI using this toolkit:

using System;
using System.Linq;
using NStack;
using Terminal.Gui;
using TestingGuiCS.Models;

namespace TestingGuiCS
{
    class Program
    {
        static void Main(string[] args)
        {
            Application.Init ();
            var top = Application.Top;

            // Creates a menubar, the item "New" has a help menu.
            var menu = new MenuBar (new MenuBarItem [] {
                new MenuBarItem ("_File", new MenuItem [] {
                    new MenuItem ("_Quit", "", () => { top.Running = false; })
                })
            });
            top.Add (menu);


            // Creates the top-level window to show
            var win = new Window (new Rect (0, 1, top.Frame.Width, top.Frame.Height-1), "Movie Db");
            top.Add (win);

            // Add some controls
            var txtSearchLbl = new Label(3, 1, "Movie Name: ");
            var txtSearch = new TextField(15, 1, 30, "");
            var forKidsOnly = new CheckBox(3, 3, "For Kids?");
            var minimumRatingLbl = new Label(25, 3, "Minimum Rating: ");
            var minimumRatingTxt = new TextField(41, 3, 10, "");
            var searchBtn = new Button(3, 5, "Filter");
            var allMoviesListView = new ListView(new Rect(4, 8, top.Frame.Width, 200), MovieDataSource.GetList(forKidsOnly.Checked, 0).ToList());
            searchBtn.Clicked = () =>
            {

                double rating = 0;
                var isDouble = double.TryParse(minimumRatingTxt.Text.ToString(), out rating);
                if(!string.IsNullOrEmpty(minimumRatingTxt.Text.ToString()) && !isDouble)
                {
                    MessageBox.ErrorQuery(30, 6, "Error", "Rating must be number");
                    minimumRatingTxt.Text = ustring.Empty;
                    return;
                }

                win.Remove(allMoviesListView);
                if (string.IsNullOrEmpty(txtSearch.Text.ToString()) || string.IsNullOrEmpty(minimumRatingTxt.Text.ToString()))
                {
                    allMoviesListView = new ListView(new Rect(4, 8, top.Frame.Width, 200),
                        MovieDataSource.GetList(forKidsOnly.Checked, rating).ToList());
                    win.Add(allMoviesListView);
                }
                else
                {
                    win.Remove(allMoviesListView);
                    win.Add(new ListView(new Rect(4, 8, top.Frame.Width, 200),
                        MovieDataSource.GetList(forKidsOnly.Checked, rating)
                            .Where(x =>
                            x.Name.Contains(txtSearch.Text.ToString(), StringComparison.OrdinalIgnoreCase)
                        ).ToList()));
                }

            };
            win.Add (
                txtSearchLbl,
                txtSearch,
                forKidsOnly,
                minimumRatingLbl,
                minimumRatingTxt,
                searchBtn,
                new Label (3, 7, "-------------Search Result--------------")
            );

            Application.Run ();
        }
    }
}

Sample app

You can grab the working sample project from GitHub.

What about cross platform UI frameworks?

Developing cross platform UIs is not officially supported by Microsoft, But there are some open source projects out there for doing so: