Introduction
In this article I will go through the steps of creating a Kanban board web application using AngularJS, WebApi, SignalR and HTML5. The idea behind this article is to show you how all these technologies can work together in harmony to produce an application that is easy to extend and manage.
A View From the Top
Figure 1 shows how the various technologies stack up with each other to produce the whole solution
Figure 1: Overall technologies used in the solution
Figure 2 shows in more details how these components interact with each other. I will explain about every component in details later in the article. You can view the full size image from here
Figure 2: Overall architecture for the solution. View full size image here
The Server
The server components of the solution are ASP.NET MVC, WebApi and SignalR server. The MVC application acts as a container for the application so there is no great deal of implementation in it apart from one controller which serves the index page. The idea of having the MVC application as a container is to benefit from the bundling feature for the scripts among other benefits like layout page. As such the explanation for server components will focus on the WebApi and the SignalR server.
WebAPi
The BoardWebApiController
holds the http service part of the solution. BoardWebApi http service contains the following services: Get, CanMove and MoveTask. Here is an explanation on every service.
The default Get method will return the columns in the Kanabn board along with their tasks. Listing 1 shows the implementation for this method. The method uses the BoardRepository to retrieve all the columns then serializes the data into JSON format then return it to the client in a form of HttpResponseMessage. I will explain about BoardRepository
later in this article.
[HttpGet]
public HttpResponseMessage Get()
{
var repo = new BoardRepository();
var response = Request.CreateResponse();
response.Content = new StringContent(JsonConvert.SerializeObject(repo.GetColumns()));
response.StatusCode = HttpStatusCode.OK;
return response;
}
Listing 1: Get http service
The second service is CanMove. This service is called before a task can be moved from one column to another. In Kanban a task can be moved from left to right (Pull) only. This service takes the source and target column ids then returns a boolean to indicate whether a task can be moved between these two columns or not. Listing 2 shows the implementation for CanMove service
[HttpGet]
public HttpResponseMessage CanMove(int sourceColId, int targetColId)
{
var response = Request.CreateResponse();
response.StatusCode = HttpStatusCode.OK;
response.Content = new StringContent(JsonConvert.SerializeObject(new { canMove = false }));
if (sourceColId == (targetColId - 1))
{
response.Content = new StringContent(JsonConvert.SerializeObject(new { canMove = true }));
}
return response;
}
Listing 2: CanMove http service
The last service is MoveTask. As the name indicates, this service will move one task from its current column to the target column. Listing 3 shows the implementation for MoveTask. Note how I am using the JObject to wrap the 2 parameters needed for this service.
[HttpPost]
public HttpResponseMessage MoveTask(JObject moveTaskParams)
{
dynamic json = moveTaskParams;
var repo = new BoardRepository();
repo.MoveTask((int)json.taskId, (int)json.targetColId);
var response = Request.CreateResponse();
response.StatusCode = HttpStatusCode.OK;
return response;
}
Listing 3: MoveTask http service
The MoveTask method on the repository will do the heavy lifting as we will see next.
The BoardRepository
The BoardRepository works on a collection of columns in memory. A column contains a list of tasks. A task holds a reference to its current column. Listing 4 shows the definition for Column and Task
public partial class Column
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public virtual List<Task> Tasks { get; set; }
}
public class Task
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public int ColumnId { get; set; }
}
Listing 4: Column and Task classes
The BoardRepository
contains the following methods: GetColumn, GetTask, GetColumns, MoveTask and UpdateColumns.
GetColumns will return the collection of columns from cache if it is available otherwise it will create a new one. GetColumn returns a column for a passed column id and GetTask returns a task for a passed task id. Listing 5 shows the implementation for GetColumns, GetColumn and GetTask
public List<Column> GetColumns()
{
if (HttpContext.Current.Cache["columns"] == null)
{
var columns = new List<Column>();
var tasks = new List<Task>();
for (int i = 1; i < 6; i++)
{
// Generating tasks is removed from this code snippet for brevity
HttpContext.Current.Cache["columns"] = columns;
}
}
return (List<Column>)HttpContext.Current.Cache["columns"];
}
public Column GetColumn(int colId)
{
return (from c in this.GetColumns()
where c.Id == colId
select c).FirstOrDefault();
}
public Task GetTask(int taskId)
{
var columns = this.GetColumns();
foreach (var c in columns)
{
foreach (var task in c.Tasks)
{
if (task.Id == taskId)
return task;
}
}
return null;
}
Listing 5: BoardRepository - 1
The remaining two methods in the BoardRepository
are MoveTask and UpdateColumns. MoveTask will take a task id and the target column id to which this task will be moving to. It then moves the task and calls UpdateColumns to update the columns collection in cache.
UpdateColumns will receive a collection of columns and updates the cache accordingly. Listing 6 shows the implementation of MoveTask and UpdateColumns.
public void MoveTask(int taskId, int targetColId)
{
var columns = this.GetColumns();
var targetColumn = this.GetColumn(targetColId);
var task = this.GetTask(taskId);
var sourceColId = task.ColumnId;
task.ColumnId = targetColId;
targetColumn.Tasks.Add(task);
var sourceCol = this.GetColumn(sourceColId);
sourceCol.Tasks.RemoveAll(t => t.Id == taskId);
columns.RemoveAll(c => c.Id == sourceColId || c.Id == targetColId);
columns.Add(targetColumn);
columns.Add(sourceCol);
this.UpdateColumns(columns.OrderBy(c => c.Id).ToList());
}
private void UpdateColumns(List<Column> columns)
{
HttpContext.Current.Cache["columns"] = columns;
}
Listing 6: BoardRepository - 2
The Client
The client consists of 2 parts, namely: UI and AngularJS. boardService contains the SignalR proxy.
The KanbanBoard UI
The Kanban board in this application consists of 4 columns and they are: to do, in progress , test and done. Kanban boards can vary heavily depending on the team but most of the time you will find these columns in one way or another in any software development kanban board. Figure 3 shows the UI for our KanbanBoard application.
Figure 3: Kanban board UI
Kanban board markup
The markup behind the board contained in index.cshtml. The markup is controlled by an AngularJS controller (boardCtrl
) which I will talk about in details later. Listing 7 shows the markup behind the board.
<div style="width:80%; margin: 0 auto 0 auto;">
<div class="row" ng-controller="boardCtrl">
<!---->
<!---->
</div>
</div>
Listing 7: Kanaban board UI markup
The boardCtrl
div consists of two div elements. The first one is the loading indicator and the second one is the columns collection div container. Listing 8 shows the definition of loading div.
<div ng-include="'/AppScript/busyModal.html'" ng-show="isLoading"></div>
Listing 8: Loading indicator
This div is using 2 AngularJS directives namely ng-include and ng-show. ng-include points to the html template which will be displayed for the loading indicator. Note how I am passing the name of the template in literal string. This tells AngularJS to treat this as a string and not as an expression. This is useful because in some scenarios you may want to include different templates depending on a given context so instead of a literal you can pass a scope variable and that variable may have different values.
ng-show on the other hand will hide or show the loading indicator based on the model variable isLoading which is set inside the boardCtrl
.
The second part is displaying the columns collection. Listing 9 shows the markup behind displaying the board's columns.
<div class="col-lg-3 panel panel-primary colStyle" id="{{col.Id}}"
kanban-board-drop="over" ng-repeat="col in columns">
<div class="panel-heading" style="margin-bottom: 10px;">
<h3 class="panel-title">{{col.Name}}</h3>
</div>
<div class="thumbnail" draggable="true" kanban-board-dragg="task"
ng-repeat="task in col.Tasks" style="margin-bottom: 10px;">
<div class="caption">
<h5><strong>{{task.Name}}</strong></h5>
<p>{{task.Description}}</p>
<p><a href="#" class="btn btn-primary btn-sm" role="button">Edit</a></p>
</div>
</div>
</div>
Listing 9: Displaying columns markup
This markup loops through a collection of columns using the ng-repeat directive. The inner ng-repeat loops through tasks in each column to display the tasks. The double curly brackets {{}} are AngularJS binding expression. I am using them to display the various information for columns and tasks. Note how I am assigning the id of the column's div container to the column id so I have a reference to it when we implement drag and drop later in this article.
The kanban-board-drop
and kanban-board-dragg
are both custom directives to handle drag and drop functionality between columns. I will explain about these directives in detail later but for now the kanban-board-drop property takes the name of the CSS class to be used with the hover effect on a the target column. The kanban-board-dragg directive takes the data item which is being dragged i.e. the task.
AngularJS
AngularJS development in this application can be divided into the following components: module, controller, service and custom directives. If you are familiar with AngularJS you will know that actually these are the main components in any AngularJS application.
The AngularJS module
The AngularJS module acts as a container for all the components that can be shipped with it. Think of it like a Dynamic Link Library (DLL) in the .NET world. Listing 10 shows the implementation for our kanbanBoardApp
module
var sulhome = sulhome || {};
sulhome.kanbanBoardApp = angular.module('kanbanBoardApp',[]);
Listing 10: kanbanBoardApp definition
I am starting by defining a namespace called sulhome. This is a good practice so your JavaScript code will not mix with other global variables which can be defined somewhere else on the page (from other libraries). The module kanbanBoardApp
is created and assigned to a variable on the namespace.
The board controller (boardCtrl)
In AngularJS, the role of the controller is to manage the model i.e. the data that will be bound back and forth from the view. boardCtrl
is doing exactly this by maintaining the board model which is in this case the columns
collection and the isLoading
variable which controls the loading indicator. Listing 11 shows the implementation of boardCtrl
.
sulhome.kanbanBoardApp.controller('boardCtrl', function ($scope, boardService) {
$scope.columns = [];
$scope.isLoading = false;
function init() {
$scope.isLoading = true;
boardService.initialize().then(function (data) {
$scope.isLoading = false;
$scope.refreshBoard();
}, onError);
};
$scope.refreshBoard = function refreshBoard() {
$scope.isLoading = true;
boardService.getColumns()
.then(function (data) {
$scope.isLoading = false;
$scope.columns = data;
}, onError);
};
$scope.onDrop =
$scope.$parent.$on("refreshBoard", function (e) {
$scope.refreshBoard();
toastr.success("Board updated successfully", "Success");
});
var onError = function (errorMessage) {
$scope.isLoading = false;
toastr.error(errorMessage, "Error");
};
init();
});
Listing 11: kanbanBoardApp definition
The boardService
gets injected into the controller. As we will see later, this service is responsible for communicating with the http service. The SignalR proxy also resides in this service (boardService).
The controller starts by defining the model variables namely columns
and isLoading
. The init function is called automatically once the controller is loaded. This function calls the service to initialise the SignalR proxy then calls refreshBoard which in turn calls the service to retrieve the columns. The onError function is used to log an error which may occur from communicating with the http service to the screen. I am using John Papa’s toastr to show the error.
The controller also listens to an event named refreshBoard
. This event will be raised from SignalR proxy and it will allow clients to refresh their board to get the latest columns. This event is raised once a task is moved from one column to another so it makes sure that all clients are in synchronisation with each other i.e. all clients have the latest board.
One method is missing from this controller which is the onDrop method. I will talk about this method when I discuss drag and drop later in this article.
The board service (boardService)
The board service is responsible for communicating with http services and SignalR server. The first 3 methods getColumns, canMoveTask and moveTask calls the corresponding http services explained earlier and return the result back to the caller i.e. the controller (boardCtrl
). Listing 12 shows the implementation for these methods.
sulhome.kanbanBoardApp.service('boardService', function ($http, $q, $rootScope) {
var getColumns = function () {
return $http.get("/api/BoardWebApi").then(function (response) {
return response.data;
}, function (error) {
return $q.reject(error.data.Message);
});
};
var canMoveTask = function (sourceColIdVal, targetColIdVal) {
return $http.get("/api/BoardWebApi/CanMove",
{ params: { sourceColId: sourceColIdVal, targetColId: targetColIdVal } })
.then(function (response) {
return response.data.canMove;
}, function (error) {
return $q.reject(error.data.Message);
});
};
var moveTask = function (taskIdVal, targetColIdVal) {
return $http.post("/api/BoardWebApi/MoveTask",
{ taskId: taskIdVal, targetColId: targetColIdVal })
.then(function (response) {
return response.status == 200;
}, function (error) {
return $q.reject(error.data.Message);
});
};
var initialize = var sendRequest =
return {
initialize: initialize,
sendRequest: sendRequest,
getColumns: getColumns,
canMoveTask: canMoveTask,
moveTask: moveTask
};
});
Listing 12: boardService -1
The methods getColumns, canMoveTask and moveTask uses AngularJS $http
service to communicate with kanban board http service. These functions process the response to return the data instead of sending the raw response. If an error occurs, the request will be rejected using the promis service ($q
) and the onError method will be called on the controller (boardCtrl
). These functions also uses the short version of $http
service which is $http.get and $http.post. As you can notice these methods matches the http verbs for get and post. Using this short version will save you from specifying the verb i.e. the verb will be GET when using $http.get and POST when using $http.post.
Listing 13 shows the remaining implementation for boardService
. The remaining implementation deals with SignalR proxy and evetns.
sulhome.kanbanBoardApp.service('boardService', function ($http, $q, $rootScope) {
var proxy = null;
var initialize = function () {
connection = jQuery.hubConnection();
this.proxy = connection.createHubProxy('KanbanBoard');
this.proxy.on('BoardUpdated', function () {
$rootScope.$emit("refreshBoard");
});
return connection.start()
.then(function (connectionObj) {
return connectionObj;
}, function (error) {
return error.message;
});
};
var sendRequest = function () {
this.proxy.invoke('NotifyBoardUpdated');
};
});
Listing 13: boardService -2
The private variable proxy
holds a reference to the SignalR proxy which is created in the initialize function. The initialize function connects to SignalR server and creates an event listener which listens to events being pushed from the SignalR server.
When a user moves a task to another column the SignalR server will push the BoardUpdated
event to all clients. This event listener then make use of AngularJS $emit
to publish the event across the AngularJS application. If you recall that the boardCtrl
contains an event listener which listens to the refreshBoard
event and calls refreshBoard method accordingly. This is how the Kanban board stays up to date across all clients.
The sendRequest method calls the SignalR server method to indicate that a task has been moved so the SIgnalR server can send a push event to all clients so they can update their board. As you will see later that this method (sendRequest) will be called from the onDrop method which is called once a task card is successfully dropped onto another column.
Drag and Drop directive
Drag and Drop functionality are programmed against HTML5 drag and drop API. So it will work as long as the browser supports HTML5 drag and drop API.
The manipulation of DOM in AngularJS should happen in a directive. For example hiding an element like a div based on a condition is accomplished by using ng-show which is an attribute directive i.e. it is used as an attribute on a DOM element. Drag and Drop functionality is no different from hiding an element, from a DOM manipulation perspective. As such it is been handled in its own directive.
Listing 14 shows the implementation for kanbanBoardDragg
directive. This directive will be applied on the item that you drag so in our application that will be the task card.
sulhome.kanbanBoardApp.directive('kanbanBoardDragg', function () {
return {
link: function ($scope, element, attrs) {
var dragData = "";
$scope.$watch(attrs.kanbanBoardDragg, function (newValue) {
dragData = newValue;
});
element.bind('dragstart', function (event) {
event.originalEvent.dataTransfer.setData("Text", JSON.stringify(dragData));
});
}
};
});
Listing 14: kanbanBoardDragg directive
The default type of directive is attribute, as such this is an attribute directive. The link property points to a function which will be called when the directive is loaded. This function receives 3 parameters:
$scope: This is the scope of the controller in our example
element: is the element on which this directive is applied, in our case this is the task card div
attrs: is an array of all the attributes on the element.
Note the usage of the $watch function on the $scope.
AngularJS will treat the value of the kanbanBoardDragg
attribute as an expression and it will reevaluate it every time it is changed. In our case this value is the task
object. The dragstart
event handler will store the dragData in the dataTransfer
attribute of the event. This will allow us to retreive this data when the item is dropped i.e. ondrop
event as you will see in the kanbanBoardDrop
directive.
Listing 15 shows the implementation for kanbanBoardDrop
directive. The dragOverClass
holds the name of the css class that will be applied on the target column to which the task will be dropped (moved). The class is set in the kanbanBoardDrop
attribute.
sulhome.kanbanBoardApp.directive('kanbanBoardDrop', function () {
return {
link: function ($scope, element, attrs) {
var dragOverClass = attrs.kanbanBoardDrop;
cancel = function (event) {
if (event.preventDefault) {
event.preventDefault();
}
if (event.stopPropigation) {
event.stopPropigation();
}
return false;
};
element.bind('dragover', function (event) {
cancel(event);
event.originalEvent.dataTransfer.dropEffect = 'move';
element.addClass(dragOverClass);
});
element.bind('drop', function (event) {
cancel(event);
element.removeClass(dragOverClass);
var droppedData = JSON.parse(event.originalEvent.dataTransfer.getData('Text'));
$scope.onDrop(droppedData, element.attr('id'));
});
element.bind('dragleave', function (event) {
element.removeClass(dragOverClass);
});
}
};
});
Listing 15: kanbanBoardDrop directive
Dragover and dragleave event handlers control the CSS class by adding or removing it from the column respectively. I am using originalEvent
instead of event
because the latter is a jQuery wrapper around the originalEvent
and hence it doesn't contain the methods which I need to use with drag and drop events.
dropEffect = 'move' is a visual effect that will appear on the target to which the task will be dropped. Other values are none, link and copy. Setting this value doesn't have any effect on the functionality but it is a UI indicator to the user to show what will be the effect of dropping a dragged item on the target. For example if you are copying items from one list to another using drag and drop then the dropEffect will be copy.
The drop event handler reads the data which was stored in the drag event then calls the onDrop method which resides in the controller (boardCtrl
). The onDrop method accepts the task object and the target column id. The source column id can be read from the task object. Listing 16 shows the implementation for the onDrop method which resides in the boardCtrl
$scope.onDrop = function (data, targetColId) {
boardService.canMoveTask(data.ColumnId, targetColId)
.then(function (canMove) {
if (canMove) {
boardService.moveTask(data.Id, targetColId).then(function (taskMoved) {
$scope.isLoading = false;
boardService.sendRequest();
}, onError);
$scope.isLoading = true;
}
}, onError);
};
Listing 16: OnDrop method
The onDrop method checks that the task can be moved and if yes it calls the moveTask service method to move the task. Once the task is moved then the isLoading
flag is set to false and sendRequest method is called. As mentioned earlier, sendRequest will call the SignalR server to send a push event for all the clients so they can update their board.
Conclusion
In this article I have explained how to build a Kanban board web application using AngularJS, SignalR, WebApi and HTML5. I hope that by the end of this article you have a good understanding on how these technologies can work together to achieve the end result.
History
V 1.0 - 24.08.2014: Created