To Do List ASP.NET Sample on GitHub
I decided to introduce "To Do List" application, because it seems to be simple - moreover, it has to be, as it is going to be built with using:
- Microsoft SQL Server 2008 database and accessing it with Entity Framework
- jQuery as robust JavaScript framework
- Underscore.js as library that simplifies a lot of generic stuff
- Require.js as modular script loader
- Backbone.js for giving strict structure to the application
- SignalR for providing real-time operations
Step first: data
Application will be working with the data represented by Task and TaskState entities. To implement it:
- Create a database named ToDoListDB
-
Run following query to create TaskState table:
USE ToDoListDB; CREATE TABLE TaskState ( [ALPHAID] NVARCHAR(128) PRIMARY KEY NOT NULL, [TITLE] NVARCHAR(128) NOT NULL )
-
Run next query to create Task table and its reference to TaskState:
USE ToDoListDB; CREATE TABLE Task ( [ID] INT PRIMARY KEY IDENTITY(1,1) NOT NULL, [TASKSTATEALPHAID] NVARCHAR(128) NOT NULL, [CONTENT] NVARCHAR(128) NOT NULL ) ALTER TABLE Task ADD CONSTRAINT FK_Task_TaskState_ALPHAID FOREIGN KEY ([TASKSTATEALPHAID]) REFERENCES TaskState([ALPHAID])
-
And finally, fill it with some random data:
USE ToDoListDB; INSERT INTO TaskState([ALPHAID], [TITLE]) VALUES ('todo', 'To Do'), ('inprogress', 'In Progress'), ('done', 'Done') INSERT INTO Task([TASKSTATEALPHAID], [CONTENT]) VALUES ('todo', 'Do something useful'), ('inprogress', 'Do a barrell roll'), ('done', 'Foo Bar'), ('todo', 'Take some rest'), ('inprogress', 'Important task'), ('done', 'Urgent stuff'), ('todo', 'Some work'), ('inprogress', 'Read a book'), ('done', 'Play guitar') -
You can check data with the following approach:
USE ToDoListDB; SELECT Task.[CONTENT] AS [Task], TaskState.[TITLE] AS [State] FROM Task INNER JOIN TaskState ON Task.[TASKSTATEALPHAID] = TaskState.[ALPHAID]
Step second: data access layer
As mentioned before, we will use Entity Framework to access data. To provide solution with data access layer, do the following:
- Create a separate class library project in Visual Studio.
- In project context menu choose "Add -> Create Element -> ADO.NET EDM Model"
- Proceed with entering required SQL Server credentials and choosing database. When completed, you will see entities diagram like this:
-
Interface contains all needed methods and properties signature:
public interface ITaskRepository { IEnumerable<Task> Select(); void Update(Task task); void Insert(Task task); void Delete(Task task); String AlphaIdToDo { get; } String AlphaIdInProgress { get; } String AlphaIdDone { get; } } -
Its implementation looks as followed:
public class TaskRepository : ITaskRepository { private ToDoListDBEntities _dbEntities; public TaskRepository() { _dbEntities = new ToDoListDBEntities(); } #region ITaskRepository public IEnumerable<Task> Select() { return _dbEntities.Tasks; } public void Update(Task task) { ApplyTaskState(task, EntityState.Modified); } public void Insert(Task task) { ApplyTaskState(task, EntityState.Added); } public void Delete(Task task) { ApplyTaskState(task, EntityState.Deleted); } public string AlphaIdToDo { get { return "todo"; } } public string AlphaIdInProgress { get { return "inprogress"; } } public string AlphaIdDone { get { return "done"; } } #endregion ITaskRepository private void ApplyTaskState(Task task, EntityState state) { _dbEntities.Tasks.Attach(task); var entry = _dbEntities.Entry(task); entry.State = state; _dbEntities.SaveChanges(); } }
Server side of the question
As application is positioned as single page one, server is not being busy with such things as Views, Action Links, Forms and so on - easier to say, it does only play a repository role and implements API controller with the following set of methods:
public class TasksController : ApiController
{
private readonly ITaskRepository _taskRepository;
public TasksController()
{
_taskRepository = new TaskRepository();
}
[HttpGet]
public IEnumerable<TaskDTO> Select()
{
var tasks = _taskRepository.Select();
return tasks.Select(ToTaskDTO);
}
[HttpPut]
public TaskDTO Update(TaskDTO taskDto)
{
Task task = ToTask(taskDto);
_taskRepository.Update(task);
return taskDto;
}
[HttpPost]
public TaskDTO Insert(TaskDTO taskDto)
{
Task task = ToTask(taskDto);
_taskRepository.Insert(task);
return taskDto;
}
[HttpDelete]
public TaskDTO Delete(TaskDTO taskDto)
{
Task task = ToTask(taskDto);
_taskRepository.Delete(task);
return taskDto;
}
private TaskDTO ToTaskDTO(Task task)
{
return new TaskDTO
{
Id = task.ID,
Content = task.CONTENT,
TaskStateAlphaId = task.TASKSTATEALPHAID
};
}
private Task ToTask(TaskDTO taskDto)
{
return new Task
{
ID = taskDto.Id,
CONTENT = taskDto.Content,
TASKSTATEALPHAID = taskDto.TaskStateAlphaId
};
}
}
As you see, server only implements interaction with data access layer throught specific HTTP wrappers. Also SignalR is used here as simple mediator: blank setup from NuGet and primitive Hub Class is all the stuff needed.
public class TaskHub : Hub
{
public void Send()
{
Clients.All.broadcastMessage();
}
}
Modular client application structure with RequireJS
Looks like we are already done with server. As I mentioned, client application is going to use a lot of frameworks and modules in order to provide a clear and strict structure to it. Using modules loader is the most approved way to put it all together.
- At first, include RequireJS main script in HTML head
- Then include following script with modules definitions. Look at project's scripts tree and the code written:
Backbone: Model, View and Collection
Advantages of MVC pattern are widely proven and used in most applications. Let's consider the things this application is made of. Model is minimal and simple:
var Task = Backbone.Model.extend({
idAttribute: "Id" //Set identity attribute to TaskDTO's "Id"
});
Collection defines the way client application interacts with server:
define(
[
'jquery',
'backbone',
'taskModel'
],
function ($, Backbone, Task) {
var Tasks = Backbone.Collection.extend({
model: Task,
url: "api/Tasks/Select",
initialize: function (signalrHub) {
this.signalrHub = signalrHub;
},
serverInsert: function (task) { //Insert on server and update collection
var that = this;
$.ajax({
type: "POST",
url: "api/Tasks/Insert",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify(task),
success: function (result) {
that.add(task);
that.signalrHub.server.send();
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Insert error!')
}
});
},
serverUpdate: function (task) { //Update changed item on server
var that = this;
$.ajax({
type: "PUT",
url: "api/Tasks/Update",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify(task),
success: function (result) {
that.signalrHub.server.send();
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Update error!')
}
});
},
serverDelete: function (task) { //Delete item on server
var that = this;
$.ajax({
type: "DELETE",
url: "api/Tasks/Delete",
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify(task),
success: function (result) {
that.remove(task);
that.signalrHub.server.send();
},
error: function (jqXHR, textStatus, errorThrown) {
alert('Delete error!')
}
});
}
});
return Tasks;
});
And View takes a responsibily of rendering page from template and handling its events. View code:
define(
[
'jquery',
'underscore',
'backbone',
'jquery.editable',
'jquery.ui',
'bootstrap'
],
function ($, _, Backbone) {
var TaskView = Backbone.View.extend({
el: $("body"), //Set rendering container to
templateHtml: $("script.tasks-board-template").html(), //Set template
initialize: function (tasksCollection) { //Set collection and bind events for it
var that = this; //Context holder
this.tasksCollection = tasksCollection; //Assigning collection
},
render: function () {
var that = this;
this.tasksCollection.fetch({
success: function (fetchedCollection) {
onCollectionFetched(fetchedCollection);
}
});
function onCollectionFetched(fetchedCollection) {
var tasksArray = fetchedCollection.toArray();
//++Filling page with content
var content = _.template(that.templateHtml, {
tasks: tasksArray
});
that.$el.html(content);
//--Filling page with content
//++Making tasks content editable
that.$("div.content").editable({
callback: function (elementData) {
//Get edited task ID
var taskId = elementData.$el.parent().attr("task-id");
//Get task from collection by id
var task = fetchedCollection.get(taskId);
//Get element content (text edited)
var content = elementData.content;
//Set modified text to item
task.set("Content", content);
//Update item on server
fetchedCollection.serverUpdate(task);
}
});
//--Making tasks content editable
//++Event fired on task adding
$('button#buttonAddNewTask').click(function () {
//Retrieve value
var taskDescription = $('#inputTaskDescription').val();
if (taskDescription) { //If present
that.tasksCollection.serverInsert({
Id: null, //Insertion needs no ID
Content: taskDescription, //From input
TaskStateAlphaId: 'todo' //Queued in "To do"
}, that);
}
});
//--Event fired on task adding
//++Event fired on taks removal
$('div.remove-handler').click(function (elementData) {
//Get task ID
var taskId = $(this).parent().attr("task-id");
//Get task from collection by id
var task = fetchedCollection.get(taskId);
//Delete item from server
fetchedCollection.serverDelete(task);
});
//--Event fired on taks removal
$("div.draggable").draggable({
handle: ".drag-handler"
});
$("div.droppable").droppable({
//hoverClass: "tdHover",
drop: function (event, ui) {
var taskId = ui.draggable.attr('task-id');
var divId = event.target.id;
var task = fetchedCollection.get(taskId);
switch (divId) {
case 'divToDo':
task.set('TaskStateAlphaId', 'todo');
break;
case 'divInProgress':
task.set('TaskStateAlphaId', 'inprogress');
break;
case 'divDone':
task.set('TaskStateAlphaId', 'done');
break;
default:
break;
}
ui.draggable.detach().css({ top: 0, left: 0 });
$(this).append(ui.draggable);
//Update item on server
fetchedCollection.serverUpdate(task);
}
});
}
return this;
}
});
return TaskView;
});
Template code:
<script type="text/template" class="tasks-board-template">Finally, all of that is required and instantiated in application's entry point:
<nav class="navbar navbar-default" role="navigation">
<a class="navbar-brand">ASP.NET MVC Realtime Sample</a>
<form class="navbar-form navbar-left" role="search">
<div class="form-group">
<input type="text" id="inputTaskDescription" class="form-control" placeholder="Enter task description">
</div>
<button type="button" class="btn btn-default" id="buttonAddNewTask">Add new task</button>
</form>
</nav>
<div id="divToDo" class="divTaskContainer droppable">
<div class="divColumnHeader">To Do</div>
<% _.each(tasks, function(task) { %>
<% if(task.get("TaskStateAlphaId") == 'todo') { %>
<div class="task draggable" task-id="<%= task.id %>">
<div class="drag-handler"></div>
<div class="content"><%= task.get("Content")%></div>
<div class="remove-handler label label-danger">Remove</div>
</div>
<%} %>
<%}); %>
</div>
<div id="divInProgress" class="divTaskContainer droppable">
<div class="divColumnHeader">In Progress</div>
<% _.each(tasks, function(task) { %>
<% if(task.get("TaskStateAlphaId") == 'inprogress') { %>
<div class="task draggable" task-id="<%= task.id %>">
<div class="drag-handler"></div>
<div class="content"><%= task.get("Content")%></div>
<div class="remove-handler label label-danger">Remove</div>
</div>
<%} %>
<%}); %>
</div>
<div id="divDone" class="divTaskContainer droppable">
<div class="divColumnHeader">Done</div>
<% _.each(tasks, function(task) { %>
<% if(task.get("TaskStateAlphaId") == 'done') { %>
<div class="task draggable" task-id="<%= task.id %>">
<div class="drag-handler"></div>
<div class="content"><%= task.get("Content")%></div>
<div class="remove-handler label label-danger">Remove</div>
</div>
<%} %>
<%}); %>
</div>
</script>
require(
[
'jquery',
'taskCollection',
'taskView',
'jquery.signalR',
'signalRHubs',
],
function ($, Tasks, TaskView) {
//Instantiate hub
var taskHub = $.connection.taskHub;
//Pass it to collection
var tasksCollection = new Tasks(taskHub);
//Create a view with collection passed
var taskView = new TaskView(tasksCollection);
//Set render on message income
taskHub.client.broadcastMessage = function () {
taskView.render();
};
//Start connection and render when ready
$.connection.hub.start().done(function () {
taskView.render();
});
}
);
Launch
Application's main page looks as follows:
It is capable of:
- Creating new record
- Editing record by double-clicking on its content
- Removing record
- Performing it all real-time: when something is changed, all other client are notified
To Do List ASP.NET Sample on GitHub



Комментариев нет:
Отправить комментарий