Features

  • ASP.NET Gantt Chart control
  • Drag and drop (task moving, resizing, link creating, row moving)
  • Dependencies (task links)
  • Task groups
  • Milestones
  • Task duration in a special column
  • Context menu for task creating and deleting
  • Visual Studio sample solution
  • C# source code
  • VB.NET source code
  • Includes a trial version of DayPilot Pro for ASP.NET WebForms

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 WebForms. Buy a license.

Create a Gantt Chart

asp.net-gantt-chart-blank.png

We will use the ASP.NET Gantt Chart control from DayPilot Pro for ASP.NET WebForms package.

<DayPilot:DayPilotGantt 
  runat="server" 
  ID="Gantt">
</DayPilot:DayPilotGantt>

New Task Row

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

Now we will enable row creating using RowCreateHandling property:

RowCreateHandling="CallBack"

CallBack handling type means the Gantt control will call the server-side RowCreate event using an AJAX CallBack.

<DayPilot:DayPilotGantt 
  runat="server" 
  ID="Gantt"
  RowCreateHandling="CallBack"
  >
</DayPilot:DayPilotGantt>

As soon as the users finishes entering the new row name the Gantt control will fire RowCreate event. We will come back to RowCreate event handler later, as soon as we connect the Gantt chart to a database.

Read more about task creating [doc.daypilot.org].

Connect the Gantt Chart to a Database

asp.net-gantt-sql-server-task-table.png

We will use the following [task] table to store the tasks in the database:

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)
);

In order to display the task hierarchy we need to following fields:

  • id - task id
  • name - task name
  • start - task start
  • end - task end
  • parent_id - id of the task parent, null for the root tasks

The Gantt chart control will let you specify the field names and load the tree hierarchy from a flat data source.

C#

protected void Page_Load(object sender, EventArgs e)
{
  if (!IsPostBack)
  {
    LoadTasksAndLinks();
  }
}

private void LoadTasks()
{
  Gantt.DataStartField = "start";
  Gantt.DataEndField = "end";
  Gantt.DataIdField = "id";
  Gantt.DataTextField = "name";
  Gantt.DataParentField = "parent_id";
  Gantt.DataMilestoneField = "milestone";
  Gantt.DataSource = Db.GetTasksFlat();
  Gantt.DataBind();
}

VB

Protected Sub Page_Load(ByVal sender As Object, ByVal e As EventArgs)
  If Not IsPostBack Then
    LoadTasksAndLinks()
  End If
End Sub

Private Sub LoadTasks()
  Gantt.DataStartField = "start"
  Gantt.DataEndField = "end"
  Gantt.DataIdField = "id"
  Gantt.DataTextField = "name"
  Gantt.DataParentField = "parent_id"
  Gantt.DataMilestoneField = "milestone"
  Gantt.DataSource = Db.GetTasksFlat()
  Gantt.DataBind()
End Sub

The Db.GetTasksFlat() method loads the flat task data.

C#

public static DataTable GetTasksFlat()
{
  SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [task] order by [ordinal], [ordinal_priority] desc", ConfigurationManager.ConnectionStrings["daypilot"].ConnectionString);
  DataTable dt = new DataTable();
  da.Fill(dt);

  return dt;
}

VB

Public Shared Function GetTasksFlat() As DataTable
  Dim da As New SqlDataAdapter("SELECT * FROM [task] order by [ordinal], [ordinal_priority] desc", ConfigurationManager.ConnectionStrings("daypilot").ConnectionString)
  Dim dt As New DataTable()
  da.Fill(dt)

  Return dt
End Function

Read more about task loading [doc.daypilot.org].

Create a New Gantt Task

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

In order to create a new task in the database, we need to add a handler for the RowCreate event. 

<DayPilot:DayPilotGantt 
  runat="server" 
  ID="Gantt"
  RowCreateHandling="CallBack"
  OnRowCreate="Gantt_OnRowCreate"
  >
</DayPilot:DayPilotGantt>

asp.net-gantt-task-created.png

This Gantt_OnRowCreate event handler will create a new database record and refresh the view.

C#

protected void Gantt_OnRowCreate(object sender, RowCreateEventArgs e)
{
  DateTime start = Gantt.StartDate;
  DateTime end = start.AddDays(1);
  string text = e.Text;

  int maxOrdinal = Db.GetMaxOrdinal(null);
  
  Db.InsertTask(start, end, text, null, maxOrdinal + 1);

  LoadTasksAndLinks();
  Gantt.UpdateWithMessage("Task created.", CallBackUpdateType.Full);
}

VB

Protected Sub Gantt_OnRowCreate(ByVal sender As Object, ByVal e As RowCreateEventArgs)
  Dim start As Date = Gantt.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()
  Gantt.UpdateWithMessage("Task created.", CallBackUpdateType.Full)
End Sub

asp.net-gantt-first-task-.png

The Db.InsertTask() method creates a new task in the database. We will use "null" as the parent id because the new task will be stored at the top level.

C#

public static int InsertTask(DateTime start, DateTime end, string name, string parent, int ordinal)
{
  using (SqlConnection con = new SqlConnection(ConfigurationManager.ConnectionStrings["daypilot"].ConnectionString))
  {
      con.Open();
      SqlCommand cmd = new SqlCommand("INSERT INTO [task] ([start], [end], [name], [parent_id], [ordinal], [ordinal_priority]) VALUES(@start, @end, @name, @parent, @ordinal, @priority)", con);
      cmd.Parameters.AddWithValue("start", start);
      cmd.Parameters.AddWithValue("end", end);
      cmd.Parameters.AddWithValue("name", name);
      cmd.Parameters.AddWithValue("parent", (object)parent ?? DBNull.Value);
      cmd.Parameters.AddWithValue("ordinal", ordinal);
      cmd.Parameters.AddWithValue("priority", DateTime.Now);
      cmd.ExecuteNonQuery();

      cmd = new SqlCommand("select @@identity;", con);
      int id = Convert.ToInt32(cmd.ExecuteScalar());
      return id;
  }
}

VB

Public Shared Function InsertTask(ByVal start As Date, ByVal [end] As Date, ByVal name As String, ByVal parent As String, ByVal ordinal As Integer) As Integer
  Using con As New SqlConnection(ConfigurationManager.ConnectionStrings("daypilot").ConnectionString)
    con.Open()
    Dim cmd As New SqlCommand("INSERT INTO [task] ([start], [end], [name], [parent_id], [ordinal], [ordinal_priority]) VALUES(@start, @end, @name, @parent, @ordinal, @priority)", con)
    cmd.Parameters.AddWithValue("start", start)
    cmd.Parameters.AddWithValue("end", [end])
    cmd.Parameters.AddWithValue("name", name)
    cmd.Parameters.AddWithValue("parent", If(DirectCast(parent, Object), DBNull.Value))
    cmd.Parameters.AddWithValue("ordinal", ordinal)
    cmd.Parameters.AddWithValue("priority", Date.Now)
    cmd.ExecuteNonQuery()

    cmd = New SqlCommand("select @@identity;", con)
    Dim id As Integer = Convert.ToInt32(cmd.ExecuteScalar())
    Return id
  End Using
End Function

Gantt Columns

asp.net-gantt-columns.png

The Gantt control can display additional task data in custom columns.

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    ...
    OnBeforeTaskRender="Gantt_OnBeforeTaskRender"
    >
    <Columns>
        <DayPilot:TaskColumn Title="Task" Property="text" Width="100" />
        <DayPilot:TaskColumn Title="Duration" Width="100" />
    </Columns>
</DayPilot:DayPilotGantt>

The columns will display data from a specified database field (Property). The column data can be further customized using BeforeTaskRender event handler. We will use it to display the task duration in the second column.

C#

protected void Gantt_OnBeforeTaskRender(object sender, BeforeTaskRenderEventArgs e)
{
  TimeSpan duration = e.End - e.Start;
  e.Row.Columns[1].Html = duration.ToString();
}

VB

Protected Sub Gantt_OnBeforeTaskRender(ByVal sender As Object, ByVal e As BeforeTaskRenderEventArgs)
  Dim duration As TimeSpan = e.End - e.Start
  e.Row.Columns(1).Html = duration.ToString()
End Sub

Read more about Gantt columns [doc.daypilot.org].

Drag and Drop Gantt Task Moving

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

Drag and drop task moving is enabled by default (TaskMoveHandling="Notify"). This feature allows the users to move the Gantt tasks in time. We need to add a TaskMove event handler to save the changes in the database:

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    
    OnTaskMove="Gantt_OnTaskMove"
    ...
    />

When TaskMoveHandling property is set to "Notify" (default value) the Gantt chart updates the task position on the client side before firing the server-side event handler. This means it isn't necessary to reload all tasks in the TaskMove event handler.

However, moving the task to a new position can affect the row header content generated using BeforeTaskRender. That's why we reload all tasks and request a full refresh of the Gantt Chart in Gantt_OnTaskMove method.

C#

protected void Gantt_OnTaskMove(object sender, TaskMoveEventArgs e)
{
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text);

  LoadTasksAndLinks();
  Gantt.Update(CallBackUpdateType.Full);
}

VB

Protected Sub Gantt_OnTaskMove(ByVal sender As Object, ByVal e As TaskMoveEventArgs)
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text)

  LoadTasksAndLinks()
  Gantt.Update(CallBackUpdateType.Full)
End Sub

Read more about drag and drop task moving [doc.daypilot.org].

Drag and Drop Gantt Task Resizing

asp.net-gantt-drag-and-drop-task-resizing.png

Drag and drop task resizing is enabled by default (TaskResizeHandling="Notify"). This feature allows the users to change the start or duration of the tasks. We need to add a TaskResize event handler to save the changes in the database:

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    
    OnTaskResize="Gantt_OnTaskResize"
    ...
    />

When TaskResizeHandling property is set to "Notify" (default value) the Gantt chart updates the task start and/or duration on the client side before firing the server-side event handler. This means it isn't necessary to reload the tasks in the TaskResize event handler.

Just as in case of EventMove, resizing the task can affect the row header content generated using BeforeTaskRender. That's why we reload all tasks and request a full refresh of the Gantt Chart in Gantt_OnTaskResize method.

C#

protected void Gantt_OnTaskResize(object sender, TaskResizeEventArgs e)
{
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text);

  LoadTasksAndLinks();
  Gantt.UpdateWithMessage("Resized", CallBackUpdateType.Full);
}

VB

Protected Sub Gantt_OnTaskResize(ByVal sender As Object, ByVal e As TaskResizeEventArgs)
  Db.UpdateTask(e.Id, e.NewStart, e.NewEnd, e.Text)

  LoadTasksAndLinks()
  Gantt.UpdateWithMessage("Resized", CallBackUpdateType.Full)
End Sub

Read more about drag and drop task resizing [doc.daypilot.org].

Drag and Drop Row Moving (Moving Tasks in the Tree Hierarchy)

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

The ASP.NET Gantt chart control supports drag and drop changes of the task hierarchy. A drag handle appears on the left side of the task header if the user hovers over its name.

asp.net-gantt-drag-and-drop-row-moving-target.png

The user can select the target position using dragging. The Gantt chart will let them place the item before or after another item, or as a child.

asp.net-gantt-drag-and-drop-row-moving-finished.png

When the task is dropped the task will be moved to the new location.

You can row moving using RowMoveHandling property:

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    ...
    RowMoveHandling="Notify"
    />

We will use two special database fields to store the task order in the database: [ordinal] and [ordinal_priority]. We will update the order after every row move and compact the sequence of [ordinal] values for every parent task.

C#

protected void Gantt_OnRowMove(object sender, RowMoveEventArgs e)
{
  const int max = Int32.MaxValue; // make sure it's greater than any other ordinal value

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

  //DataRow sourceRow = DbGetTask(e.Source.Id);
  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 Sub Gantt_OnRowMove(ByVal sender As Object, ByVal e As RowMoveEventArgs)
  Const max As Integer = Int32.MaxValue ' make sure it's greater than any other ordinal value

  Dim sourceParent As Task = Gantt.Tasks.FindParent(e.Source)
  Dim targetParent As Task = Gantt.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

The [ordinal] field stores the task order for the same parent task (stored in [parent_id]). The [ordinal_priority] stores the last update date and time and is used temporarily during the reordering. It keeps the algorithm simpler.

We also have to update the task loading method to use the [ordinal] field for sorting the tasks.

C#

public static DataTable GetTasksFlat()
{
  SqlDataAdapter da = new SqlDataAdapter("SELECT * FROM [task] order by [ordinal], [ordinal_priority] desc", ConfigurationManager.ConnectionStrings["daypilot"].ConnectionString);
  DataTable dt = new DataTable();
  da.Fill(dt);

  return dt;
}

VB

Public Shared Function GetTasksFlat() As DataTable
  Dim da As New SqlDataAdapter("SELECT * FROM [task] order by [ordinal], [ordinal_priority] desc", ConfigurationManager.ConnectionStrings("daypilot").ConnectionString)
  Dim dt As New DataTable()
  da.Fill(dt)

  Return dt
End Function

Read more about drag and drop row moving [doc.daypilot.org].

Task Context Menu

asp.net-gantt-task-context-menu.png

We will add a task context menu that will let the users perform the following actions:

  • Create a child task
  • Edit task details in a modal dialog
  • Convert the task to a milestone
  • Delete the task

The context menu (and its items) is defined using a DayPilotMenu control:

<DayPilot:DayPilotMenu runat="server" id="ContextMenuTask" ClientIDMode="Static">
    <MenuItems>
        <DayPilot:MenuItem Text="Add Child Task..." Action="JavaScript" JavaScript="createChild(this.source);"></DayPilot:MenuItem>
        <DayPilot:MenuItem Text="-"></DayPilot:MenuItem>
        <DayPilot:MenuItem Text="Edit..." Action="JavaScript" JavaScript="edit(this.source)"></DayPilot:MenuItem>
        <DayPilot:MenuItem Text="Convert to Milestone" Action="CallBack" Command="ToMilestone"></DayPilot:MenuItem>
        <DayPilot:MenuItem Text="Delete" Action="CallBack" Command="Delete"></DayPilot:MenuItem>
    </MenuItems>
</DayPilot:DayPilotMenu>

We will assign the context menu to the Gantt control using ContextMenuRowID property:

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    ...
    ContextMenuRowID="ContextMenuTask"
    OnRowMenuClick="Gantt_OnRowMenuClick"
    />

The context menu items with Action="CallBack" will be handled on the server side using RowMenuClick event.

C#

protected void Gantt_OnRowMenuClick(object sender, RowMenuClickEventArgs e)
{
  switch (e.Command)
  {
      case "Delete":
          Db.DeleteTaskWithChildren(e.Task.Id);
          LoadTasksAndLinks();
          Gantt.Update();
          break;
      case "ToMilestone":
          Db.ToMilestone(e.Task.Id);
          LoadTasksAndLinks();
          Gantt.Update();
          break;
      case "ToTask":
          Db.ToTask(e.Task.Id);
          LoadTasksAndLinks();
          Gantt.Update();
          break;
  }
}

VB

Protected Sub Gantt_OnRowMenuClick(ByVal sender As Object, ByVal e As RowMenuClickEventArgs)
  Select Case e.Command
    Case "Delete"
      Db.DeleteTaskWithChildren(e.Task.Id)
      LoadTasksAndLinks()
      Gantt.Update()
    Case "ToMilestone"
      Db.ToMilestone(e.Task.Id)
      LoadTasksAndLinks()
      Gantt.Update()
    Case "ToTask"
      Db.ToTask(e.Task.Id)
      LoadTasksAndLinks()
      Gantt.Update()
  End Select
End Sub

By default the row context menu can be invoked using a right click. In order to provide a visual hint to the users we will add an active area to the row using BeforeTaskRender event handler.

C#

protected void Gantt_OnBeforeTaskRender(object sender, BeforeTaskRenderEventArgs e)
{        
  TimeSpan duration = e.End - e.Start;
  e.Row.Columns[1].Html = duration.ToString();

  e.Row.Areas.Add(new Area().Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuTask").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"));
}

VB

Protected Sub Gantt_OnBeforeTaskRender(ByVal sender As Object, ByVal e As BeforeTaskRenderEventArgs)
  Dim duration As TimeSpan = e.End - e.Start
  e.Row.Columns(1).Html = duration.ToString()

  e.Row.Areas.Add((New Area()).Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuTask").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"))

End Sub

asp.net-gantt-milestone-context-menu.png

The context menu actions should be different for task groups and milestones. We will create a special context menu control for each task type.

<DayPilot:DayPilotMenu runat="server" id="ContextMenuGroup" ClientIDMode="Static">
  <MenuItems>
      <DayPilot:MenuItem Text="Add Child Task..." Action="JavaScript" JavaScript="createChild(this.source);"></DayPilot:MenuItem>
      <DayPilot:MenuItem Text="-"></DayPilot:MenuItem>
      <DayPilot:MenuItem Text="Edit..." Action="JavaScript" JavaScript="edit(this.source)"></DayPilot:MenuItem>
      <DayPilot:MenuItem Text="Delete" Action="CallBack" Command="Delete"></DayPilot:MenuItem>
  </MenuItems>
</DayPilot:DayPilotMenu>
  
<DayPilot:DayPilotMenu runat="server" id="ContextMenuMilestone" ClientIDMode="Static">
  <MenuItems>
      <DayPilot:MenuItem Text="Edit..." Action="JavaScript" JavaScript="edit(this.source)"></DayPilot:MenuItem>
      <DayPilot:MenuItem Text="Convert to Task" Action="CallBack" Command="ToTask"></DayPilot:MenuItem>
      <DayPilot:MenuItem Text="Delete" Action="CallBack" Command="Delete"></DayPilot:MenuItem>
  </MenuItems>
</DayPilot:DayPilotMenu>

We will modify BeforeTaskRender again and assign the context menu depending on the task type.

C#

protected void Gantt_OnBeforeTaskRender(object sender, BeforeTaskRenderEventArgs e)
{        
  TimeSpan duration = e.End - e.Start;
  e.Row.Columns[1].Html = duration.ToString();


  switch (e.Type)
  {
      case TaskType.Milestone:
          e.Row.ContextMenuClientName = "ContextMenuMilestone";
          e.Row.Areas.Add(new Area().Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuMilestone").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"));
          break;
      case TaskType.Group:
          e.Row.ContextMenuClientName = "ContextMenuGroup";
          e.Row.Areas.Add(new Area().Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuGroup").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"));
          break;
      case TaskType.Task:
          e.Row.Areas.Add(new Area().Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuTask").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"));
          break;

  }
}

VB

Protected Sub Gantt_OnBeforeTaskRender(ByVal sender As Object, ByVal e As BeforeTaskRenderEventArgs)
  Dim duration As TimeSpan = e.End - e.Start
  e.Row.Columns(1).Html = duration.ToString()


  Select Case e.Type
    Case TaskType.Milestone
      e.Row.ContextMenuClientName = "ContextMenuMilestone"
      e.Row.Areas.Add((New Area()).Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuMilestone").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"))
    Case TaskType.Group
      e.Row.ContextMenuClientName = "ContextMenuGroup"
      e.Row.Areas.Add((New Area()).Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuGroup").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"))
    Case TaskType.Task
      e.Row.Areas.Add((New Area()).Width(14).Height(14).Right(2).Top(2).ContextMenu("ContextMenuTask").Style("cursor: pointer; box-sizing: border-box; background: white; border: 1px solid #ccc; background-repeat: no-repeat; background-position: center center; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAABASURBVChTYxg4wAjE0kC8AoiFQAJYwFcgjocwGRiMgPgdEP9HwyBFDkCMAtAVY1UEAzDFeBXBAEgxQUWUAgYGAEurD5Y3/iOAAAAAAElFTkSuQmCC);"))

  End Select
End Sub

Creating Milestones

asp.net-gantt-milestone.png

Milestone is a special type of task that can be used to mark a specified point in time.

We won't let the users create the milestones directly - they are able to convert a new task to a milestone using a context menu.

We store the milestone flag in the database in [milestone] boolean field:

C#

private void LoadTasks()
{
  Gantt.DataStartField = "start";
  Gantt.DataEndField = "end";
  Gantt.DataIdField = "id";
  Gantt.DataTextField = "name";
  Gantt.DataParentField = "parent_id";
  Gantt.DataMilestoneField = "milestone";
  Gantt.DataSource = Db.GetTasksFlat();
  Gantt.DataBind();
}

VB

Private Sub LoadTasks()
  Gantt.DataStartField = "start"
  Gantt.DataEndField = "end"
  Gantt.DataIdField = "id"
  Gantt.DataTextField = "name"
  Gantt.DataParentField = "parent_id"
  Gantt.DataMilestoneField = "milestone"
  Gantt.DataSource = Db.GetTasksFlat()
  Gantt.DataBind()
End Sub

Task Dependencies (Links)

asp.net-gantt-link-created.png

The Gantt chart supports links that show task dependencies. Common link types (Finish-to-Start, Finish-to-Finish, Start-to-Start, Start-to-Finish) are supported.

Links can be loaded using Links property.

C#

private void LoadLinks()
{
  Gantt.Links.Clear();
  DataTable data = Db.GetLinks();
  foreach (DataRow row in data.Rows)
  {
      Link link = new Link(Convert.ToString(row["from"]), Convert.ToString(row["to"]));
      link.Id = Convert.ToString(row["id"]);
      link.Type = LinkTypeParser.Parse((string) row["type"]);
      Gantt.Links.Add(link);
  }
}

VB

Private Sub LoadLinks()
  Gantt.Links.Clear()
  Dim data As DataTable = Db.GetLinks()
  For Each row As DataRow In data.Rows
    Dim link As New Link(Convert.ToString(row("from")), Convert.ToString(row("to")))
    link.Id = Convert.ToString(row("id"))
    link.Type = LinkTypeParser.Parse(DirectCast(row("type"), String))
    Gantt.Links.Add(link)
  Next row
End Sub

Drag and Drop Link Creating

asp.net-gantt-link-creating.png

The Gantt control supports creating task links using drag and drop. This feature is enabled by default (LinkCreateHandling="Notify"). We need to create a LinkCreate event handler in order to create a DB record for the new link.

<DayPilot:DayPilotGantt
  runat="server" 
  ID="Gantt"
  ...
  OnLinkCreate="Gantt_OnLinkCreate"
  />

The Gantt_OnLinkCreate updates the database. We need to reload the events on the client side. Although the link would be created on the client side in the default "Notify" handling mode it would have no ID associated to it.

C#

protected void Gantt_OnLinkCreate(object sender, LinkCreateEventArgs e)
{
  Db.InsertLink(e.FromId, e.ToId, e.Type.ToString());

  LoadTasksAndLinks();
  Gantt.Update(CallBackUpdateType.Full);
}

VB

Protected Sub Gantt_OnLinkCreate(ByVal sender As Object, ByVal e As LinkCreateEventArgs)
  Db.InsertLink(e.FromId, e.ToId, e.Type.ToString())

  Dim fullRefresh As Boolean = True
  If fullRefresh Then
    LoadTasksAndLinks()
    Gantt.Update(CallBackUpdateType.Full)
  End If

End Sub

Link Deleting

asp.net-gantt-link-deleting.png

We will let the users delete links using a context menu. First, we will create a context menu control and link it to the Gantt control using ContextMenuLinkID property.

<DayPilot:DayPilotGantt
    runat="server" 
    ID="Gantt"
    ...
    ContextMenuLinkID="ContextMenuLink"
    OnLinkMenuClick="Gantt_OnLinkMenuClick"
    />

<DayPilot:DayPilotMenu ID="ContextMenuLink" runat="server">
    <DayPilot:MenuItem Text="Delete" Action="Callback" Command="Delete"></DayPilot:MenuItem>
</DayPilot:DayPilotMenu>

The "Delete" menu item will fire LinkMenuClick event on the server side.

C#

protected void Gantt_OnLinkMenuClick(object sender, LinkMenuClickEventArgs e)
{
  switch (e.Command)
  {
      case "Delete":
          Db.DeleteLink(e.Id);
          LoadTasksAndLinks();
          Gantt.Update();
          break;
  }
}

VB

Protected Sub Gantt_OnLinkMenuClick(ByVal sender As Object, ByVal e As LinkMenuClickEventArgs)
  Select Case e.Command
    Case "Delete"
      Db.DeleteLink(e.Id)
      LoadTasksAndLinks()
      Gantt.Update()
  End Select
End Sub

Read more about link context menu [doc.daypilot.org].

Displaying Task Progress

asp.net-gantt-chart-progress-complete.png

The Gantt chart includes built-in support for displaying task progress. It is displayed graphically in the complete bar at the top of the task box. The task also displays the complete percentage as text in the task box.

The complete percentage can be loaded directly from the database using DataCompleteField property:

C#

private void LoadTasks()
{
  Gantt.DataStartField = "start";
  Gantt.DataEndField = "end";
  Gantt.DataIdField = "id";
  // ...
  Gantt.DataCompleteField = "complete";
  Gantt.DataSource = Db.GetTasksFlat();
  Gantt.DataBind();
}

VB

Private Sub LoadTasks()
  Gantt.DataStartField = "start"
  Gantt.DataEndField = "end"
  Gantt.DataIdField = "id"
  ' ...
  Gantt.DataCompleteField = "complete"
  Gantt.DataSource = Db.GetTasksFlat()
  Gantt.DataBind()
End Sub

Read more about task percent complete [doc.daypilot.org].