Features

This tutorial shows how to use DayPilot ASP.NET MVC Gantt chart control.

  • Hierarchy of tasks
  • Task groups
  • Task dependencies
  • Drag and drop support: task moving and resizing, reordering tasks in the hierarchy, link creating
  • Adding a new task using a special row
  • Custom columns with additional data (name, id, duration)
  • Visual Studio 2013 solution
  • SQL Server 2014 database
  • ASP.NET MVC 5
  • C# source code
  • VB.NET source code

License

Licensed for testing and evaluation purposes. You can use the source code of the tutorial if you are a licensed user of DayPilot Pro for ASP.NET MVC. Buy a license.

Gantt Initialization: MVC View

Include the client-side DayPilot library in the view (daypilot-all.min.js):

<script src="@Url.Content("~/Scripts/DayPilot/daypilot-all.min.js")" type="text/javascript"></script>

Add the Gantt Chart to the MVC view using DayPilotGantt Html helper:

Index.cshtml

@using DayPilot.Web.Mvc;


@Html.DayPilotGantt("Gantt", new DayPilotGanttConfig
{
  BackendUrl = Url.Action("Backend", "Gantt")
})

Index.vbhtml

@imports DayPilot.Web.Mvc


@Html.DayPilotGantt("Gantt", New DayPilotGanttConfig With
{
  .BackendUrl = Url.Action("Backend", "Gantt")
})

The BackendUrl property is required. It points to the MVC controller that will handle the AJAX requests.

Gantt Initialization: MVC Controller

Now we will create a new controller for handling the AJAX events.

The controller will contain "Backend" action:

C#

public class GanttController : Controller
{

  public ActionResult Backend()
  {
    // ...
  }

}

VB

Public Class GanttController
  Inherits Controller

  Public Function Backend() As ActionResult
      ' ...
  End Function

End Class

We will create a new class called "Gantt" derived from DayPilotGantt. This class will transform the incoming AJAX requests into server-side events.

C#

public class GanttController : Controller
{

  public ActionResult Backend()
  {
      return new Gantt().CallBack(this);
  }

  class Gantt : DayPilotGantt
  {
  }

}

VB

Public Class GanttController
  Inherits Controller

  Public Function Backend() As ActionResult
      Return (New Gantt()).CallBack(Me)
  End Function

  Private Class Gantt
      Inherits DayPilotGantt

  End Class

End Class

If you run your ASP.NET MVC project now it will display a blank Gantt chart with no tasks and no timeline:

asp.net-mvc-gantt-chart-initialization.png

No we will use the controller to configure the Gantt chart timeline.

  1. We will add an event handler for the "Init" event. This event is called right after the Gantt chart initialization on the client side. It will let us load the Gantt chart data and adjust the configuration.
  2. First, we set the start date to October 1, 2014 and time range to 60 days. 
  3. The UpdateWithMessage() call sends the changes to the client side and displays a message  ("Welcome!") using the built-in message bar.

C#

class Gantt : DayPilotGantt
{
  protected override void OnInit(InitArgs e)
  {
    StartDate = new DateTime(2014, 10, 1);
    Days = 60;
    UpdateWithMessage("Welcome!");
  }
}

VB

Private Class Gantt
  Inherits DayPilotGantt

  Protected Overrides Sub OnInit(ByVal e As InitArgs)
      StartDate = New Date(2014, 10, 1)
      Days = 60
      UpdateWithMessage("Welcome!")
  End Sub

End Class

This is what you will see with this configuration:

asp.net-mvc-gantt-timeline.png

Loading Gantt Tasks

asp.net-mvc-gantt-chart-load-tasks.png

Now we will add task loading code to the OnInit event handler.

C#

class Gantt : DayPilotGantt
{
  protected override void OnInit(InitArgs e)
  {
    StartDate = new DateTime(2014, 10, 1);
    Days = 60;

    LoadTasks();

    UpdateWithMessage("Welcome!");
  }

  private void LoadTasks()
  {

      TasksFromEnumerable(
          Db.GetTasksFlat().Rows,
          new TaskFieldMappings()
          {
              TextField = "name",
              IdField = "id",
              StartField = "start",
              EndField = "end",
              ParentField = "parent_id",
              MilestoneField = "milestone",
              TagFields = "id"
          }
      );
  }            
}

VB

Public Class GanttController
  Inherits Controller

  Public Function Backend() As ActionResult
      Return (New Gantt()).CallBack(Me)
  End Function

  Private Class Gantt
      Inherits DayPilotGantt

      Protected Overrides Sub OnInit(ByVal e As InitArgs)
          StartDate = New Date(2014, 10, 1)
          Days = 60

          LoadTasks()

          UpdateWithMessage("Welcome!")
      End Sub

      Private Sub LoadTasks()

          TasksFromEnumerable(Db.GetTasksFlat().Rows, New TaskFieldMappings() With {
            .TextField = "name", 
            .IdField = "id", 
            .StartField = "start", 
            .EndField = "end", 
            .ParentField = "parent_id", 
            .MilestoneField = "milestone", 
            .TagFields = "id"})
            
      End Sub

  End Class

End Class

The tasks are stored in a single database table with the following structure:

asp.net-mvc-gantt-database-schema-task.png

Table DDL:

CREATE TABLE [dbo].[task] (
    [id]               INT          IDENTITY (1, 1) NOT NULL,
    [name]             VARCHAR (50) NULL,
    [start]            DATETIME     NOT NULL,
    [end]              DATETIME     NOT NULL,
    [parent_id]        INT          NULL,
    [ordinal]          INT          NULL,
    [ordinal_priority] DATETIME     NULL,
    [milestone]        BIT          DEFAULT ((0)) NOT NULL,
    CONSTRAINT [PK_task] PRIMARY KEY CLUSTERED ([id] ASC)
);

At this moment we need the following columns:

  • id - task ID
  • name - task name
  • start - task start
  • end - task end
  • parent_id - ID of the parent task

These fields allow us to load task start and end date and its location in the task hierarchy.

You can also build the tree manually using the Tasks property like this:

C#

private void LoadTasks()
{
    Task group = Tasks.AddGroup("Group 1", "G1");
    group.Children.Add(NewTask("Task 1", "1", DateTime.Today, DateTime.Today.AddDays(2)));
    group.Children.Add(NewTask("Task 2", "2", DateTime.Today, DateTime.Today.AddDays(2)));
    group.Children.AddMilestone("Milestone 1", "3", DateTime.Today.AddDays(2));
}

VB

Private Sub LoadTasks()
  Dim group As Task = Tasks.AddGroup("Group 1", "G1")
  group.Children.Add(NewTask("Task 1", "1", Date.Today, Date.Today.AddDays(2)))
  group.Children.Add(NewTask("Task 2", "2", Date.Today, Date.Today.AddDays(2)))
  group.Children.AddMilestone("Milestone 1", "3", Date.Today.AddDays(2))
End Sub

But that would require manually loading the children for each task. We will use the built-in TasksFromEnumerable() method that will let us load the whole hierarchy from a flat table:

C#

 TasksFromEnumerable(
          Db.GetTasksFlat().Rows,
          new TaskFieldMappings()
          {
              TextField = "name",
              IdField = "id",
              StartField = "start",
              EndField = "end",
              ParentField = "parent_id"
          }
      );

VB

TasksFromEnumerable(Db.GetTasksFlat().Rows, New TaskFieldMappings() With {
  .TextField = "name", 
  .IdField = "id", 
  .StartField = "start", 
  .EndField = "end", 
  .ParentField = "parent_id"})

The helper Db.GetTasksFlat() method returns a DataTable with all the records. In this example we are using the DataTable rows as the data source but it can be any Enumerable of custom objects.

The second parameter specifies the field mappings.

The TasksFromEnumerable() method loads the task tree into the Tasks property.

Loading Links (Task Dependencies)

asp.net-mvc-gantt-chart-loading-links.png

We will load the task links using a similar procedure.

C#

public class GanttController : Controller
{

  public ActionResult Backend()
  {
      return new Gantt().CallBack(this);
  }

  class Gantt : DayPilotGantt
  {
      
      protected override void OnInit(InitArgs e)
      {
          StartDate = new DateTime(2014, 10, 1);
          Days = 60;

          LoadTasks();
          LoadLinks();

          UpdateWithMessage("Welcome!");
      }
      
      // ...

      private void LoadLinks()
      {
          LinksFromEnumerable(
              Db.GetLinks().Rows,
              new LinkFieldMappings()
              {
                  IdField = "id",
                  FromField = "from",
                  ToField = "to",
                  TypeField = "type"
              });
      }
  }
}

VB

Public Class GanttController
  Inherits Controller

  Public Function Backend() As ActionResult
      Return (New Gantt()).CallBack(Me)
  End Function

  Private Class Gantt
      Inherits DayPilotGantt

      Protected Overrides Sub OnInit(ByVal e As InitArgs)
          StartDate = New Date(2014, 10, 1)
          Days = 60
          
          LoadTasks()
          LoadLinks()
          
          UpdateWithMessage("Welcome!")
      End Sub

      Private Sub LoadLinks()
          LinksFromEnumerable(Db.GetLinks().Rows, New LinkFieldMappings() With {
            .IdField = "id", 
            .FromField = "from", 
            .ToField = "to", 
            .TypeField = "type"})
      End Sub

  End Class

End Class

The links are stored in "link" table:

asp.net-mvc-gantt-database-schema-link.png

DDL:

CREATE TABLE [dbo].[link] (
    [id]   INT           IDENTITY (1, 1) NOT NULL,
    [from] INT           NOT NULL,
    [to]   INT           NOT NULL,
    [type] NVARCHAR (50) NOT NULL,
    PRIMARY KEY CLUSTERED ([id] ASC)
);

The fields:

  • id - link ID
  • from - ID of the source task
  • to - ID of the target task
  • type - link type ("FinishToStart", "FinishToFinish", "StartToStart", "FinishToFinish")

The links can also be loaded directly using Links property but we  use the LinksFromEnumerable() method to load them directly from the data source.

C#

LinksFromEnumerable(
  Db.GetLinks().Rows,
  new LinkFieldMappings()
  {
      IdField = "id",
      FromField = "from",
      ToField = "to",
      TypeField = "type"
  });

VB

Private Sub LoadLinks()
  LinksFromEnumerable(Db.GetLinks().Rows, New LinkFieldMappings() With {
    .IdField = "id", 
    .FromField = "from", 
    .ToField = "to", 
    .TypeField = "type"})
End Sub

Gantt Chart Columns

asp.net-mvc-gantt-columns.png

The Gantt chart control lets us display custom data in additional columns. By default it displays a single column with the task name.

We want to display three columns:

  • name
  • id
  • duration

First, we need to define the columns in the MVC view

MVC View (.cshtml)

@Html.DayPilotGantt("Gantt", new DayPilotGanttConfig
{
  // ...
  Columns =
  {
      new TaskColumn { Title = "Name", Property = "text"},
      new TaskColumn { Title = "Id", Property = "id"},
      new TaskColumn { Title = "Duration"}
  }
  
})

MVC View (.vbhtml)

@Html.DayPilotGantt("Gantt", New DayPilotGanttConfig With
{
    ' ...
    .Columns = New TaskColumnCollection From
    {
        New TaskColumn With { .Title = "Name", .Property = "text"},
        New TaskColumn With { .Title = "Id", .Property = "id"},
        New TaskColumn With { .Title = "Duration", .Property="milestone"}
    }
})

The Title will be used for the column header and Property will be used to load the default data for each row.

In the controller, we need to make sure that the property is specified in custom tag fields. We can load custom field as tags using TagFields property of TaskFieldMappings. We add the "id" field because we want to display it in the second column:

C#

private void LoadTasks() {

  TasksFromEnumerable(
      Db.GetTasksFlat().Rows, 
      new TaskFieldMappings()
      {
          TextField = "name",
          IdField = "id",
          StartField = "start",
          EndField = "end",
          ParentField = "parent_id",

          TagFields = "id"
      }
  );
}

VB

TasksFromEnumerable(Db.GetTasksFlat().Rows, New TaskFieldMappings() With {
  .TextField = "name", 
  .IdField = "id", 
  .StartField = "start", 
  .EndField = "end", 
  .ParentField = "parent_id", 
  .TagFields = "id"})

You can also build the column content manually. We will use this option to display the task duration in the third column.

Use OnBeforeTaskRender event:

C#

protected override void OnBeforeTaskRender(BeforeTaskRenderArgs e)
{
  e.Row.Columns[2].Html = (e.End - e.Start).ToString();
}

VB

Protected Overrides Sub OnBeforeTaskRender(ByVal e As BeforeTaskRenderArgs)
  e.Row.Columns(2).Html = (e.End - e.Start).ToString()
End Sub

Drag and Drop: Task Moving

asp.net-mvc-gantt-drag-drop-task-moving.png

Drag and drop task moving is enabled by default. The only thing we need to do is add a server-side event handler that will save the change in the database:

C#

protected override void OnTaskMove(TaskMoveArgs e)
{
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text);
  LoadTasksAndLinks();
  Update();
}

VB

Protected Overrides Sub OnTaskMove(ByVal e As TaskMoveArgs)
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text)
  LoadTasksAndLinks()
  Update()
End Sub

Drag and Drop: Task Resizing

asp.net-mvc-gantt-drag-drop-resizing.png

The same applies to task resizing. Just add an event handler that saves the updated task start and end.

C#

protected override void OnTaskResize(TaskResizeArgs e)
{
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text);
  LoadTasksAndLinks();
  Update();
}

VB

Protected Overrides Sub OnTaskResize(ByVal e As TaskResizeArgs)
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text)
  LoadTasksAndLinks()
  Update()
End Sub

Drag and Drop: Link Creating

asp.net-mvc-gantt-new-link.png

The Gantt chart control supports drag and drop link creating. It is enabled by default.

The OnLinkCreate event handler allows us to save the new link in the database.

C#

protected override void OnLinkCreate(LinkCreateArgs e)
{
  Db.InsertLink(e.FromId, e.ToId, e.Type.ToString());
  LoadTasksAndLinks();
  Update();
}

VB

Protected Overrides Sub OnLinkCreate(ByVal e As LinkCreateArgs)
  Db.InsertLink(e.FromId, e.ToId, e.Type.ToString())
  LoadTasksAndLinks()
  Update()
End Sub

Drag and Drop: Row Moving

asp.net-mvc-gantt-drag-drop-row-moving.png

The Gantt chart supports drag and drop changes to the task hierarchy. 

It's not enabled by default and we need to update it in the MVC view config using RowMoveHandling property:

Index.cshtml

@Html.DayPilotGantt("Gantt", new DayPilotGanttConfig
{
    BackendUrl = Url.Action("Backend", "Gantt"),

    // ...
    RowMoveHandling = RowMoveHandlingType.Notify
})

Index.vbhtml

@Html.DayPilotGantt("Gantt", New DayPilotGanttConfig With
{
  .BackendUrl = Url.Action("Backend", "Gantt"),
  ' ...
  .RowMoveHandling = RowMoveHandlingType.Notify
})

The Notify handling type means the row position will be updated on the client side first. The server-side event handler will be fired after the change. No refresh is required. 

You may only need to force the update if the hierarchy update affect some of the row or task properties that need to be updated from the server side.

The server-side OnRowMove handler is a bit more complicated because we need to update the task order correctly:

C#

protected override void OnRowMove(RowMoveArgs e)
{
  const int max = Int32.MaxValue; // make sure it's greater than any other ordinal value

  Task sourceParent = Tasks.FindParent(e.Source);
  Task targetParent = Tasks.FindParent(e.Target);

  DataRow targetRow = Db.GetTask(e.Target.Id);

  string targetParentId = targetParent != null ? targetParent.Id : null;
  string sourceParentId = sourceParent != null ? sourceParent.Id : null;

  int targetOrdinal = (int)targetRow["ordinal"];

  switch (e.Position)
  {
      case RowMovePosition.Before:
          Db.UpdateTaskParent(e.Source.Id, targetParentId, targetOrdinal);
          break;
      case RowMovePosition.After:
          Db.UpdateTaskParent(e.Source.Id, targetParentId, targetOrdinal + 1);
          break;
      case RowMovePosition.Child:
          Db.UpdateTaskParent(e.Source.Id, e.Target.Id, max);
          targetParentId = e.Target.Id;
          break;
      case RowMovePosition.Forbidden:
          break;
      default:
          throw new ArgumentOutOfRangeException();
  }

  Db.CompactOrdinals(sourceParentId);
  if (sourceParentId != targetParentId)
  {
      Db.CompactOrdinals(targetParentId);
  }
}

VB

Protected Overrides Sub OnRowMove(ByVal e As RowMoveArgs)
  Const max As Integer = Int32.MaxValue ' make sure it's greater than any other ordinal value

  Dim sourceParent As Task = Tasks.FindParent(e.Source)
  Dim targetParent As Task = Tasks.FindParent(e.Target)

  'DataRow sourceRow = DbGetTask(e.Source.Id);
  Dim targetRow As DataRow = Db.GetTask(e.Target.Id)

  Dim targetParentId As String = If(targetParent IsNot Nothing, targetParent.Id, Nothing)
  Dim sourceParentId As String = If(sourceParent IsNot Nothing, sourceParent.Id, Nothing)

  Dim targetOrdinal As Integer = DirectCast(targetRow("ordinal"), Integer)

  Select Case e.Position
      Case RowMovePosition.Before
          Db.UpdateTaskParent(e.Source.Id, targetParentId, targetOrdinal)
      Case RowMovePosition.After
          Db.UpdateTaskParent(e.Source.Id, targetParentId, targetOrdinal + 1)
      Case RowMovePosition.Child
          Db.UpdateTaskParent(e.Source.Id, e.Target.Id, max)
          targetParentId = e.Target.Id
      Case RowMovePosition.Forbidden
      Case Else
          Throw New ArgumentOutOfRangeException()
  End Select

  Db.CompactOrdinals(sourceParentId)
  If sourceParentId <> targetParentId Then
      Db.CompactOrdinals(targetParentId)
  End If
End Sub

We use the ordinal and ordinal_priority fields to store the task order. The OnRowMove handler updates the fields and compacts the numbers in the ordinal field after the changes are made.

Adding a New Task Inline

asp.net-mvc-gantt-new-task.png

The Gantt chart control includes built-in support for adding a new task.

You need to enable the new task row in the MVC View using RowCreateHandling property:

Index.cshtml

@Html.DayPilotGantt("Gantt", new DayPilotGanttConfig
{
    BackendUrl = Url.Action("Backend", "Gantt"),
    
    // ...
    RowCreateHandling = RowCreateHandlingType.CallBack
})

Index.vbhtml

@Html.DayPilotGantt("Gantt", New DayPilotGanttConfig With
{
  .BackendUrl = Url.Action("Backend", "Gantt"),
  ' ...
  .RowCreateHandling = RowCreateHandlingType.CallBack
})

When the user finishes entering the new task name the Gantt chart will fire OnRowCreate event on the server side:

C#

protected override void OnRowCreate(RowCreateArgs e)
{
  DateTime start = StartDate;
  DateTime end = start.AddDays(1);
  string text = e.Text;
  int maxOrdinal = Db.GetMaxOrdinal(null);

  Db.InsertTask(start, end, text, null, maxOrdinal + 1);
  LoadTasksAndLinks();
  Update();
}

VB

Protected Overrides Sub OnRowCreate(ByVal e As RowCreateArgs)
  Dim start As Date = StartDate
  Dim [end] As Date = start.AddDays(1)
  Dim text As String = e.Text
  Dim maxOrdinal As Integer = Db.GetMaxOrdinal(Nothing)

  Db.InsertTask(start, [end], text, Nothing, maxOrdinal + 1)
  LoadTasksAndLinks()
  Update()
End Sub

The handler will create a new task at the top level and refresh the Gantt chart using Update().