Introduction
This project demonstrates implementing Gantt chart functionality in Silverlight. The primary purpose is to allow a user to quickly and easily enter data that relates to two elements. In this example, the two elements are date and person; however, the code can be adapted for other relations.
Features of the Gantt Chart
This Silverlight project is not a full featured Gantt chart such as Microsoft Project. It demonstrates only the most basic functionality, and is provided as a starting point for your own projects.
The features it includes are:
- Displays a full year on a scrollable window
- Allows a "Date Box" to be created for each day for up to 15 rows
- The text for each date in the 15 rows can be changed
- Each "Date Box" can be resized to add or subtract days
- Each "Date Box" can be deleted
- The "Date Boxes" on each row cannot overlap
- When a year is changed, the "Day Boxes" entered on it are saved and redisplayed when the year is re-selected
The Layout
The Silverlight Gantt chart is made up of the following elements:
- ToolBar - A Silverlight
Canvas
control that slides to the right and left. It slides behind a "Clipping Window" called ToolBarWindow
that only shows part of the ToolBar at a time.
- Year Selector - A Silverlight
ComboBox
control that allows the year to be changed.
- Scroll Bar - A Silverlight
ScrollBar
control that is programmatically linked to the ToolBar. It allows the user to move the ToolBar.
- Row Label - A series of 15 Silverlight
Textbox
controls that provide a label for each ToolBar row.
- Month Box - A series of 12 Silverlight controls that represent the months.
- Day Box - A series of Silverlight controls that represent a day of the month. The controls are shaded when their day falls on a weekend.
- Date Box - A Silverlight control that represents date(s) placed on the ToolBar.
The Grid
When the Silverlight Gantt control loads, it creates some default data:
#region CreateDefaultData
private void CreateDefaultData()
{
colDateBoxAllYears = new List<datebox>();
DateBox objDateBox = new DateBox(3,
Convert.ToDateTime("1/10/2009"),
Convert.ToDateTime("1/14/2009"));
colDateBoxAllYears.Add(objDateBox);
DateBox objDateBox2 = new DateBox(4,
Convert.ToDateTime("1/5/2009"),
Convert.ToDateTime("1/6/2009"));
colDateBoxAllYears.Add(objDateBox2);
}
#endregion
It instantiates two DateBox
controls, passing the row and the "Start Date" and the "End Date". It then adds these to a Generic List
called colDateBoxAllYears
.
#region DisplayYear
private void DisplayYear(string strYear)
{
ToolBar.Children.Clear();
List<dateplannermonth> colDatePlannerMonths =
DatePlannerMonth.GetMonths(strYear);
double StartPosition = (double)0;
foreach (DatePlannerMonth objDatePlannerMonth in colDatePlannerMonths)
{
AddMonthToToolbar(objDatePlannerMonth, strYear, StartPosition);
StartPosition = StartPosition + objDatePlannerMonth.MonthWidth;
}
DisplayGridlines();
LoadEventsForYear();
}
#endregion
This collection is used when the DisplayYear
method is called. This method adds the months and the days for the current year to the ToolBar
(the Grid
). It also adds the grid lines and any dates for the current year that are in the colDateBoxAllYears
collection.
#region UpdateToolBarPosition
private void UpdateToolBarPosition(Point Point)
{
double dCurrentPosition = (Point.X - StartingDragPoint.X);
if ((dCurrentPosition < 0) & (dCurrentPosition > -8234))
{
Canvas.SetLeft(ToolBar, Point.X - StartingDragPoint.X);
ctlScrollBar.Value = dCurrentPosition * -1;
}
}
#endregion
The ToolBar
is moved by altering its Canvas.SetLeft
position. ctlScrollBar.Value
moves the ScrollBar
control position so that it stays in sync with the ToolBar
.
The ScrollBar
The ScrollBar
control moves the ToolBar
using the following code:
#region ctlScrollBar_Scroll
private void ctlScrollBar_Scroll(object sender,
System.Windows.Controls.Primitives.ScrollEventArgs e)
{
Point Point = new Point(e.NewValue, 0);
Canvas.SetLeft(ToolBar, Point.X * -1);
}
#endregion
Months and Days
The MonthBox
control is primarily composed of a Silverlight StackPanel
control with 31 DayBox
controls. When each month control is created, it is sized to only show the correct amount of days for the month it represents.
#region GetMonthWidth
private static double GetMonthWidth(DateTime dtMonthYear)
{
double dWidth = 744;
int intDaysInMonth = DateTime.DaysInMonth(dtMonthYear.Year,
dtMonthYear.Month);
int intDaysToSubtract = (31 - intDaysInMonth);
if (intDaysToSubtract > 0)
{
dWidth = dWidth - (24 * intDaysToSubtract);
}
return dWidth;
}
#endregion
The ASP.NET DateTime.DaysInMonth
method will automatically handle complex calculations such as leap years.
The Year DropDown
When the year is changed, the following code executes:
#region dlYear_SelectionChanged
private void dlYear_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (colDateBoxAllYears != null)
{
List<datebox> colDateBoxNotCurrentYear =
colDateBoxAllYears.AsEnumerable().Where(x => x.DayBoxStart.Year !=
dtCurrentYear.Year).Cast<datebox>().ToList();
List<datebox> colDateBoxCurrentYear =
ToolBar.Children.AsEnumerable().Where(x => x.GetType().Name ==
"DateBox").Cast<datebox>().ToList();
colDateBoxAllYears = new List<datebox>();
foreach (DateBox objDateBox in colDateBoxNotCurrentYear)
{
colDateBoxAllYears.Add(objDateBox);
}
foreach (DateBox objDateBox in colDateBoxCurrentYear)
{
colDateBoxAllYears.Add(objDateBox);
}
dtCurrentYear =
Convert.ToDateTime(String.Format("1/1/{0}",
GetSelectedYear()));
DisplayYear(GetSelectedYear());
}
}
#endregion
The code uses LINQ to get all the DateBox
controls in the colDateBoxAllYears
collection that are not for the current year; it then uses LINQ to get the DateBox
controls that are on the ToolBar
(these are the boxes for the current year).
It then combines the two to build a final collection. This is saved in the ccolDateBoxAllYears
collection, and the newly selected year is then displayed.
The DateBox Control
The DateBox
control is used to indicate the dates selected on the ToolBar
. It is composed of a Silverlight Rectangle
with a Silverlight Canvas
on the left side and the right site. The left and right side Canvas
es are used to determine when a user is trying to drag the control wider or smaller.
public DateBox(int parmBoxRow, DateTime parmDayBoxStart, DateTime parmDayBoxStop)
{
InitializeComponent();
_BoxRow = parmBoxRow;
_DayBoxStart = parmDayBoxStart;
_DayBoxStop = parmDayBoxStop;
TimeSpan tsBoxDaysDifference = _DayBoxStop - _DayBoxStart;
int intBoxDays = tsBoxDaysDifference.Days;
this.BoxSize = (intBoxDays * 24) + 24;
SetToolTip();
}
When the control is instantiated, the current row and start and stop dates are saved, and the width of the box is set based on the amount of days.
#region SetToolTip
private void SetToolTip()
{
ToolTipService.SetToolTip(BoxRetangle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
ToolTipService.SetToolTip(LeftSideHandle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
ToolTipService.SetToolTip(RightSideHandle, String.Format("{0} - {1}",
_DayBoxStart.ToShortDateString(),
_DayBoxStop.ToShortDateString()));
}
#endregion
It also uses the ToolTipService
object to create the display that shows the dates for the DateBox
when the user hovers the mouse over the elements.
private void LeftButton_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point tmpPoint = e.GetPosition(null);
List<uielement> hits =
(List<uielement>)System.Windows.Media.VisualTreeHelper
.FindElementsInHostCoordinates(tmpPoint, this);
List<datebox> colDateBox = ToolBar.Children.AsEnumerable().Where(
x => x.GetType().Name == "DateBox").Cast<datebox>().ToList();
foreach (DateBox objDateBox in colDateBox)
{
if (hits.Contains(objDateBox))
{
Point DateBoxPoint = e.GetPosition(objDateBox);
TimeSpan tsBoxDays = objDateBox.DayBoxStop - objDateBox.DayBoxStart;
if (DateBoxPoint.X <= 2 ||
(DateBoxPoint.X >= ((tsBoxDays.Days + 1) * 24) - 2))
{
objResizingDateBox = objDateBox;
StartingCanvasDragPoint = GetMousePosition(ToolBar, e);
if (DateBoxPoint.X <= 2)
{
strResizingDateBoxSide = ''Left'';
}
else
{
strResizingDateBoxSide = ''Right'';
}
}
else
{
ShowPopup(objDateBox);
return;
}
}
}
When the user clicks on the ToolBar
, the FindElementsInHostCoordinates
method is used to find all the elements that are at the present mouse position. Note, the horizontal and vertical gridlines have IsHitTestVisible="false"
to improve performance (it means they will be ignored).
If an existing DateBox
is detected, the code checks to see if one of the sides of the DateBox
is detected because that would indicate the user wishes to drag the DateBox
wider or smaller.
if (CheckTolerance(StartingDragPoint, EndingDragPoint))
{
Point objPoint = BoxClicked(EndingDragPoint);
InsertBox(objPoint, 0);
}
When a user lifts the mouse button after clicking on the ToolBar
and there are no other elements there, the CheckTolerance
method is used to determine if the mouse moved more than two "points" in either direction.
#region InsertBox
private void InsertBox(Point objPoint, int intDays)
{
int intX = Convert.ToInt32(objPoint.X);
int intY = Convert.ToInt32(objPoint.Y);
Point boxPoint = new Point((intX * 24 - 24), (intY * 24 - 28));
if (intY > 2)
{
DateBox objDateBox = new DateBox(intY, GetBoxDate(intX, GetSelectedYear()),
GetBoxDate(intX + intDays, GetSelectedYear()));
Canvas.SetLeft(objDateBox, boxPoint.X);
Canvas.SetTop(objDateBox, boxPoint.Y);
ToolBar.Children.Add(objDateBox);
}
}
#endregion
If the mouse did not move past the "tolerance", a new box is instantiated and added to the Toolbar
using: ToolBar.Children.Add(objDateBox)
.
The Popup
When the user clicks directly on a DateBox
, the Popup
control is used to display the date range of the DateBox
and to allow the user the option to delete the DateBox
.
#region CreatePopup
private void CreatePopup()
{
objDateBoxPopup = new Popup();
objDateBoxPopup.Name = "DeletePopup";
objDateBoxPopup.Child = new DateBoxPopup();
objDateBoxPopup.SetValue(Canvas.LeftProperty, 150d);
objDateBoxPopup.SetValue(Canvas.TopProperty, 150d);
objDateBoxPopup.HorizontalOffset = 25;
objDateBoxPopup.VerticalOffset = 25;
ToolBarWindow.Children.Add(objDateBoxPopup);
objDateBoxPopup.IsOpen = false;
}
#endregion
When the Silverlight Gantt chart first loads, a Popup
is created and a DateBoxPopup
control is placed inside it.
#region ShowPopup
private void ShowPopup(DateBox objDateBox)
{
if (objDateBoxPopup.IsOpen == false)
{
objDateBoxPopup.CaptureMouse();
DateBoxPopup GanttPopUpBox =
(DateBoxPopup)objDateBoxPopup.FindName("GanttPopUpBox");
GanttPopUpBox.objDateBox = objDateBox;
objDateBoxPopup.IsOpen = true;
}
}
#endregion
When the Popup
needs to appear, a instance of the associated DateBox
is set as a property of the DateBoxPopup
control (that is contained in the Popup
).
#region btnDelete_Click
private void btnDelete_Click(object sender, RoutedEventArgs e)
{
Canvas ToolBar = (Canvas)this.LayoutRoot.FindName("ToolBar");
ToolBar.Children.Remove(_objDateBox);
ClosePopup();
}
#endregion
If the Delete button is clicked, the DateBoxPopup
control will already have an instance of the DateBox
control so it can delete it by removing it from the ToolBar
.
#region ClosePopup
private void ClosePopup()
{
Popup objPopup = (Popup)this.LayoutRoot.FindName("DeletePopup");
objPopup.ReleaseMouseCapture();
objPopup.IsOpen = false;
}
#endregion
When the Popup
is "closed", its IsOpen
property is simply set to false
.
The Power of the Mouse
The mouse is a very efficient input device. A user is able to move their hand and click much faster than they can usually type. A control such as a Gantt chart allows the user to indicate two things at one time with a single mouse click. In this example, the click represents:
- "I want to set this person to be reserved on this day".
However, you can adapt this code to represent other things such as:
- "I want this room reserved for these days".
- "I want this product to be placed in this part of the store" (you would not use the
MonthBox
es if you wanted to do this; instead, you would show each section of the store at the top of the control).
To Save you Some Time
Hopefully, this control will save you time when implementing your own Gantt chart functionality. Primarily, it saves you time because it performs the necessary calculations to prevent DateBox
es from overlapping.
To save the data, simply save the colDateBoxAllYears
collection. To load the data, simply pass the colDateBoxAllYears
collection to the control on start-up and the call the DisplayYear
method.