duminică, 13 februarie 2011

Razor, ASP.NET MVC3 and a quick funny little app

I’ve been trying lately to keep up with Microsoft and the .NET framework but it’s been a loosing battle since the get go. The pace of which Microsoft churns out new features for .NET is amazingly scary if you are not some uber-programmer. So I’ve settled for just some areas, gave up my dreams of learning WPF and Silverlight and now I am focused on ASP.NET MVC (3).

I also have a problem with time management so, to kill  two birds with one stone, here is a little web app that keeps a time log of your actions. The idea is simple: When ever you start doing something that falls into a category or another, simply click the start button for the appropriate category and when you stop… yes, click the stop button. The difference in time (seconds, or minutes, or whatever you choose) is stored in a database, along with the date and time when you completed the action. Here is a little screen:

TimeLogScreenShot

So what do we need?

  1. ASP.NET MVC3
  2. JQuery Timers. I got mine from here: http://plugins.jquery.com/project/timers
  3. (Optional). The excellent CSS3 GitHub-like buttons from http://css3buttons.michaelhenriksen.dk/

Let’s begin. First create a new ASP.NET MVC3 Internet Application and choose Razor as your view engine. Razor is so cool, but totally different from the traditional aspx view engine. You will see why when you play a bit with it.

Create a new database with just two tables in it:

TimeLogDBSchema 

And here is the script for creating the two tables:

USE [TimeLog]
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_Duration]
END


End
GO
IF EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [DF_LogEntry_CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] DROP CONSTRAINT [FK_LogEntry_Category]
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
DROP TABLE [dbo].[LogEntry]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
DROP TABLE [dbo].[Category]
GO
/****** Object: Table [dbo].[Category] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Category]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[Category](
[CategoryId] [int] IDENTITY(1,1) NOT NULL,
[CategoryName] [varchar](100) NULL,
CONSTRAINT [PK_Category] PRIMARY KEY CLUSTERED
(
[CategoryId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
SET ANSI_PADDING OFF
GO
/****** Object: Table [dbo].[LogEntry] Script Date: 02/13/2011 09:15:48 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[LogEntry]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[LogEntry](
[LogId] [int] IDENTITY(1,1) NOT NULL,
[CategoryId] [int] NULL,
[Duration] [decimal](8, 2) NULL,
[CreatedOn] [datetime] NULL,
CONSTRAINT [PK_LogEntry] PRIMARY KEY CLUSTERED
(
[LogId] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
END
GO
/****** Object: Default [DF_LogEntry_Duration] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_Duration]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_Duration]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_Duration] DEFAULT ((0)) FOR [Duration]
END


End
GO
/****** Object: Default [DF_LogEntry_CreatedOn] Script Date: 02/13/2011 09:15:48 ******/
IF Not EXISTS (SELECT * FROM sys.default_constraints WHERE object_id = OBJECT_ID(N'[dbo].[DF_LogEntry_CreatedOn]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
Begin
IF NOT EXISTS (SELECT * FROM dbo.sysobjects WHERE id = OBJECT_ID(N'[DF_LogEntry_CreatedOn]') AND type = 'D')
BEGIN
ALTER TABLE [dbo].[LogEntry] ADD CONSTRAINT [DF_LogEntry_CreatedOn] DEFAULT (getdate()) FOR [CreatedOn]
END


End
GO
/****** Object: ForeignKey [FK_LogEntry_Category] Script Date: 02/13/2011 09:15:48 ******/
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] WITH CHECK ADD CONSTRAINT [FK_LogEntry_Category] FOREIGN KEY([CategoryId])
REFERENCES [dbo].[Category] ([CategoryId])
GO
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[dbo].[FK_LogEntry_Category]') AND parent_object_id = OBJECT_ID(N'[dbo].[LogEntry]'))
ALTER TABLE [dbo].[LogEntry] CHECK CONSTRAINT [FK_LogEntry_Category]
GO


Now, go to the Models folder in your MVC app and create a new Entity Framework model based on the database you’ve just created. And that takes care of the back-end.
 
Time for some controller action. There is only one method in your home controller you need to create:
[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

You also need to change a bit the Index action to return all the categories in the database. Your home controller should look like this by now:
TimeLogEntities entities = new TimeLogEntities();
public ActionResult Index()
{
ViewBag.Message = "Welcome to ASP.NET MVC!";
List<Category> categories = entities.Categories.ToList<Category>();
return View(categories);
}


[AcceptVerbs(HttpVerbs.Post)]
public JsonResult InsertTimeLog(int categoryId, int value)
{
LogEntry le = new LogEntry();
Category cat = entities.Categories.First<Category>(c => c.CategoryId == categoryId);
le.Category = cat;
le.Duration = value;
le.CreatedOn = DateTime.Now;
entities.AddToLogEntries(le);
entities.SaveChanges();
return null;
}

public ActionResult About()
{
return View();
}
}

Delete the default Index View in the Views/Home Folder and create a new one based on the above controller.

Ok, one more step to go: The index view. This is where I learnt to love Razor… The index view should look like this (explanations below the code):

@model IEnumerable<MvcApplication3.Models.Category>

@{
ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
<span>Categories</span>
</p>
<div class = "categories">
<ul id="CategoryList">

@foreach (var item in Model) {
<li>
<div class="@item.CategoryName.Replace(" ", "")">
<p>
<span class="Category" >@item.CategoryName</span>
<span style="padding-left:20px"></span>
<a class="big button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")">Start</a>
<span style="padding-left:20px"></span>
<a style="padding-left:30px;padding-right:30px" class="big negative button" id="@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")" >Stop</a>
<span id="@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))" style="padding-left:20px">0</span>
</p>
</div>

</li>
}

</ul>
</div>
@foreach (var item in Model)
{
<script type="text/javascript">
   1:  
   2:  var categories = $("div.categories");
   3:      var @String.Concat("active", @item.CategoryName.Replace(" ", "")) = false;
   4:      $('.@item.CategoryName.Replace(" ", "")', categories).find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Start")').css("cursor", "pointer").click(function (i) {
   5:          if (!@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
   6:              $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(0);
   7:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
   8:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').everyTime(1000, '@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))', function (i) {
   9:                  $("#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))").text(i);
  10:              });
  11:          }
  12:      }).end().find('#@String.Concat(@item.CategoryName.Replace(" ", ""),"Stop")').css("cursor", "pointer").click(function (i) {
  13:          if (@String.Concat("active", @item.CategoryName.Replace(" ", ""))) {
  14:              @String.Concat("active", @item.CategoryName.Replace(" ", "")) = !@String.Concat("active", @item.CategoryName.Replace(" ", ""));
  15:              $(this).parents("div").find('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').stopTime('@String.Concat("controlled", @item.CategoryName.Replace(" ", ""))');
  16:              $.ajax({
  17:                  url: "/Home/InsertTimeLog/", type: "POST", dataType: "json",
  18:                  data: { categoryId: @item.CategoryId, value: $('#@String.Concat("timeSpent", @item.CategoryName.Replace(" ", ""))').text() },
  19:                  success: function () {
  20:                    ;;
  21:                  }
  22:              })
  23:          }
  24:      });
  25:   
</script>
}

Basically, what this view does is:
  1. For each category insert a little markup with category name, start/stop buttons and a little something to display the time passed
  2. For each category, generate a javascript to handle the start/stop timer and an ajax call to insert the time spent into the database.

That’s allSmile


Caveats: There are many things missing here: a repository, IoC, it only works for one user, a controller and views for categories and a bit many more, but all I wanted was a simple app and this is about as simple as you can get.

Niciun comentariu:

Trimiteți un comentariu