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
Комментариев нет:
Отправить комментарий