Introduction

The focus in this article is data binding time data from SQL-Server as time data type which is represented client site as a TimeSpan.

SQL Server 2008 introduces a TIME data type which allows us to store the time without the date. TIME will do an implicit conversion from DATETIME and retain only the time portion.  TIME will implicitly accept strings in most common time formats.

For perspective, the following shows dateime output (not to be confused with  datetime2) to time field output between two different tables.

Example of a datetime field in a SQL-Server database table. The table has a primary key and datetime fields.

CREATE TABLE dbo.Events
(
 EventID INT IDENTITY(1, 1)  NOT NULL ,
 StartDate DATETIME NOT NULL ,
 EndDate DATETIME NOT NULL
);

SELECT statement to return data.

SELECT  EventID ,
  StartDate ,
  EndDate
FROM   DateTimeDatabase.dbo.Events;

Results for the SELECT where the highlight is the time portion of the datetime.

To separate date from time.

SELECT  EventID ,
  StartDate ,
  CONVERT(VARCHAR(5), StartDate, 108) AS StartTime ,
  EndDate ,
  CONVERT(VARCHAR(5), EndDate, 108) AS EndTime
FROM   dbo.Events;

The next example uses time fields rather than datetime.

Table structure

CREATE TABLE dbo.TimeTable
(
 id INT IDENTITY(1, 1)  NOT NULL ,
 FirstName NVARCHAR(MAX) NULL ,
 LastName NVARCHAR(MAX) NULL ,
 StartTime TIME(7) NULL ,
 EndTime TIME(7) NULL
)

Select

SELECT  id ,
  FirstName ,
  LastName ,
  StartTime ,
  EndTime
FROM   dbo.TimeTable;

Results

The following SELECT formats time fields with AM-PM.

SELECT  id ,
  FirstName ,
  LastName ,
  StartTime ,
  FORMAT(CAST(StartTime AS DATETIME), 'hh:mm tt') BeginningTime ,
  FORMAT(CAST(EndTime AS DATETIME), 'hh:mm tt') EnddingTime ,
  EndTime
FROM   dbo.TimeTable;

Results

In the last query the following provided hour:minutes AM/PM.

FORMAT(CAST(StartTime AS DATETIME), 'hh:mm tt') BeginningTime

Back in a project, a conversion would take only hour:minutes as highlighted in the first row start time above. You can also use the following to show just hour:minutes which can be helpful when data being returned and converted does not match what you expected.

FORMAT(CAST(StartTime AS DATETIME), 'hh:mm') BeginningTime

Note that when returning a time field into your program its data type is time yet when debugging viewing just the time field can be confusing while using hh:mm rather than hh:mm tt or raw is easier for debugging purposes.

When loading the above table into a DataTable you would access (in this case) StartTime as follows.

row.Field<TimeSpan>("StartTime")

If instead a data reader was just to populate a list container were in the following reader is a SqlDataReader and StartTime is a property for a class instance.

const string selectStatement = "SELECT  id,FirstName,LastName,StartTime,EndTime " +
        "FROM dbo.TimeTable;";
StartTime = reader.GetTimeSpan(3)

Class used for the above.

public class Person
{
  public int Id { get; set; }
  public string FirstName { get; set; }
  public string LastName { get; set; }
  public TimeSpan StartTime { get; set; }
  public TimeSpan EndTime { get; set; }
}

With the above information for a grounding with datetime and time fields it's time to work with data in a Visual Studio project.

With Entity Framework StartTime and EndTime are Nullable TimeSpan. And the extension methods presented can be applied to these fields as they are still TimeSpan where access is via Value and a null check can be done since they are nullable via timeTableItem.StartTime.HasValue.

public partial class TimeTable
{
    public int id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public Nullable<System.TimeSpan> StartTime { get; set; }
    public Nullable<System.TimeSpan> EndTime { get; set; }
}



Using time over DateTime

Using time may be the best choice when data time is not required or is redundant when storing a start and end time or for things like elapsed time.

Using DateTime with controls on a Windows form are straightforward. To collect date time information a developer places a DateTimePicker on the form, set a few properties and in turn is ready to receive your input. Backend operations on the selected date time may proof challenging if there are start and end dates to deal although the DateTimePicker deals nicely with a foolproof control to collect date and time or date without time or time without date.

If a task requires presenting records from a database table where one field is a DateTime a DataTimePicker can be data bound to a field.

The following DateTimePicker binds to a DateTime field from a BindingSource which has a DataTable as its DataSource.

StartTimeDateTimePicker.DataBindings.Add("Text", _bindingSource, "StartDate");

DateTimePicker for collecting time only

To collect a time value from a user, set a DateTimePicker Format property to "Time" and optionally set ShowUpDown property to true. 

To get the current value access TimeOfDay property below Value.

Console.WriteLine(StartTimeDateTimePicker.Value.TimeOfDay);

If the current selected time was 9:00 AM the above line would return 09:00:00.  The property TimeOfDay type is TimeSpan which is suitable to update a value in a SQL-Server database table which will be discussed later. If the TimeOfDay value needs to be presented in a readable format the TimeSpan can be added to a DateTime object and formatted with .ToString method for the DateTime object. In this case a language extension makes sense placed in a library within either a class within your project or in a class library which can be used in other projects.

public static string Formatted(this TimeSpan sender, string format = "hh:mm tt")
{
  return DateTime.Today.Add(sender).ToString(format);
}

To use the extension method.

Console.WriteLine(StartTimeDateTimePicker.Value.TimeOfDay.Formatted());

Which will now return 09:00 AM. If the leading zero is unwanted the extension method can change the format as follows which excludes the leading zero.

public static string Formatted(this TimeSpan sender, string format = "h:mm tt")
{
  return DateTime.Today.Add(sender).ToString(format);
}

DateTimePicker data binding issues with time only

The standard DateTimePicker when data bound to a time only field will properly read time for each row where the row may be a field in a DataRow of a DataTable or an instance of a class object when bound to a List<T> but update the that DateTimePicker value, tab to another control and the time reverts back to the unedited value. There are optional parameters for data binding which simply cause more issues which require more work then one should need to do.

A suitable workaround is to use a BindingSource component, set the BindingSource DataSource to a List<T> or a DataTable. Subscribe to the BindingSource event, CurrentChanged. In the following code snippet the CurrentChange event calls a method HandleCurrentChanged which sets the proper field value (which is done rather than data binding as mentioned above). This only handles reading but taking care of when a user changes a value in the DataTimePicker. The HandleCurrentChanged event is used so when data is initially loaded a call is made to show the first row's time value as  the CurrentChanged event is not called until a record movement is made from a search or one of the move methods of the BindingSource e.g. MoveNext, MoveLast etc.

private void _bindingSource_CurrentChanged(object sender, EventArgs e)
{
  HandleCurrentChanged();
}
/// <summary>
/// Handle manually updating StartTime on current change of the BindingSource
/// </summary>
private void HandleCurrentChanged()
{
  var row = ((DataRowView) _bindingSource.Current).Row;
  StartTimeDateTimePicker.Value = row.Field<TimeSpan>("StartTime").ToDateTime();
}

To update the changed value subscribe to the DateTimePicker's ValueChanged event.

/// <summary>
/// Manually update StartTime
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void StartTimeDateTimePicker_ValueChanged(object sender, EventArgs e)
{
  var row = ((DataRowView)_bindingSource.Current).Row;
  row.SetField("StartTime", StartTimeDateTimePicker.Value.TimeOfDay);
}

To update the DateTimePicker value back to the database table get the underlying (in this case) DataRow.

var row = ((DataRowView)_bindingSource.Current).Row;

Create a method to do the SQL UPDATE which will require the primary key for the current record and the value to update. The call would look like this (which is included in the accompanying source code). If the update is not successful in this case the user is alerted there was an issue, otherwise they are not informed of a successful update.

Alternate solutions

Create a custom DateTimePicker which handles data binding properly which for most developers is more work then the return investment. Another choice is using a third party control to handle time only with data binding which are always in a library with more controls then most developers need although using third party libraries fosters consistency if the developer always uses the library.

Third party control

Here is one custom TimePicker which has many options on how to present time.

Keeping with the example above, the TimePicker returns a DateTime and to get the Time the following code is required.

radTimePicker1.Value.Value;

Custom ComboBox

Create a custom ComboBox control which understands time. Implementation should include showing time for each hour of a day, each hour of the day with options to include quarter and half hour increments. Providing quarter and half hour increments go with what many time systems use rather than using 9:04 AM systems usually want 9:00 AM which goes similarly for increments of an hour.  A utility class which be appropriate for creating hour ranges etc. 

/// <summary>
/// Used to create a string array which represent time in a day.
/// </summary>
/// <remarks>
/// To get a range of TimeSpan rather than a string array.
///    var hours = new Hours();
///    var range = hours.Range(TimeIncrement.Quarterly).ToList().Select(time => time.TimeSpan());
/// In the above example a specific increment is used. This could easliy be a flexible language
/// extension method.
/// </remarks>
public class Hours
{
    public string[] Range(TimeIncrement pTimeIncrement = TimeIncrement.Hourly)
    {
 
        const string timeHhMmTtformat = "hh:mm tt";
 
        IEnumerable<DateTime> hours = Enumerable.Range(0, 24)
            .Select((index) => (DateTime.MinValue.AddHours(index)));
 
        var timeList = new List<string>();
 
        foreach (var dateTime in hours)
        {
 
            timeList.Add(dateTime.ToString(timeHhMmTtformat));
 
            if (pTimeIncrement == TimeIncrement.Quarterly)
            {
                timeList.Add(dateTime.AddMinutes(15).ToString(timeHhMmTtformat));
                timeList.Add(dateTime.AddMinutes(30).ToString(timeHhMmTtformat));
                timeList.Add(dateTime.AddMinutes(45).ToString(timeHhMmTtformat));
            }
            else if (pTimeIncrement == TimeIncrement.HalfHour)
            {
                timeList.Add(dateTime.AddMinutes(30).ToString(timeHhMmTtformat));
            }
        }
 
        return timeList.ToArray();
 
    }
}

Other considerations are what properties should be available such as TimeSpan for a selected item, Hour and minute properties so that nothing need be done in the user code. Various methods to find and set a item in the custom ComboBox by time string or TimeSpan. Sorting should be disabled as there is no reason to sort time values as they are needed to be presented in time order.

Custom ComboBox responsible for displaying time only.

public class TimeComboBox : ComboBox
{
  private bool _shown = false;
  public TimeComboBox()
  {
  DropDownStyle = ComboBoxStyle.DropDownList;
 
  if (!DesignMode)
  {
    var hours = new Hours();
    Items.AddRange(hours.Range(Increment));
  }
 
  Size = new Size(80, 21);
 
  }
  /// <summary>
  /// Disable sorting
  /// </summary>
#pragma warning disable 108,114
  public bool Sorted => false;
#pragma warning restore 108,114
  /// <summary>
  /// Set current item in the ComboBox using a TimeSpan.
  /// If the value passed in is not in the ComboBox -1 is returned.
  /// </summary>
  /// <param name="pTime"></param>
  /// <returns>Index of item or -1 if not found</returns>
  public int SetCurrentItem(TimeSpan pTime)
  {
  var dateTimeime = DateTime.Today.Add(pTime);
  var displayTime = dateTimeime.ToString("hh:mm tt");
  var index = FindString(displayTime);
  SelectedIndex = index;
 
  return index;
 
  }
  /// <summary>
  /// Set current item by string which represents a valid TimeSpan
  /// </summary>
  /// <param name="pTime"></param>
  /// <returns></returns>
  public int SetCurrentItem(string pTime)
  {
  if (TimeSpan.TryParse(pTime, out var time))
  {
    var dateTimeime = DateTime.Today.Add(time);
    var displayTime = dateTimeime.ToString("hh:mm tt");
    var index = FindString(displayTime);
    if (index > -1)
    {
    SelectedIndex = index;
    }
     
    return index;
  }
  else
  {
    return -1;
  }
     
  }
 
  private TimeSpan _timeSpan;
  /// <summary>
  /// Get current selected item as a TimeSpan
  /// </summary>
  [Browsable(false)]
  public TimeSpan TimeSpan => Convert.ToDateTime(Text.TrimStart('0')).TimeOfDay;
 
  private int _hours;
  /// <summary>
  /// Get hour for selected item
  /// </summary>
  [Browsable(false)]
  public int Hours => TimeSpan.Hours;
 
  private int _Minutes;
  /// <summary>
  /// Get minutes for selected item
  /// </summary>
  [Browsable(false)]
  public int Minutes => TimeSpan.Minutes;
 
  /// <summary>
  /// Determine if current selected item is AM
  /// </summary>
  [Browsable(false)]
  public bool IsAM
  {
  get
  {
    var check = new DateTime(
    DateTime.Now.Year,
    DateTime.Now.Month,
    DateTime.Now.Day,
    TimeSpan.Hours,
    TimeSpan.Minutes, 0);
 
    return check.ToString("tt") == "AM";
 
  }
  }
  /// <summary>
  /// Determine if current selected item is PM
  /// </summary>
  [Browsable(false)]
  public bool IsPM => !IsAM;
  /// <summary>
  /// Provides formatting e.g. 3:45 PM
  /// </summary>
  public string Formatted => TimeSpan.Formatted();
 
  protected static TimeIncrement ParentIncrement { get; set; }
 
  private TimeIncrement _increment;
  /// <summary>
  /// Get/set increment <see cref="TimeIncrement"/>
  /// </summary>
  [Category("Behavior"), Browsable(true), Description("Time increment")]
  public TimeIncrement Increment
  {
  set
  {
    _increment = value;
    ParentIncrement = value;
    Items.Clear();
 
    var hours = new Hours();
    Items.AddRange(hours.Range(Increment));
  }
  get
  {
    return _increment;
  }
  }
 
  private static string _hour = "";
 
  [Category("Behavior"), Browsable(true), Editor(typeof(Editor), typeof(UITypeEditor)), Description("Hour get/set")]
  public string Time
  {
  set
  {
    _hour = value;
    SetHour();
  }
  get
  {
    return _hour;
  }
  }
  private bool SetHour()
  {
  bool success = false;
  if (string.IsNullOrWhiteSpace(Time))
  {
    Time = "00:00";
  }
 
  int result = FindString(Time);
  if (result > -1)
  {
    SelectedIndex = result;
    var item = Items.Count;
    success = true;
  }
  else
  {
    SelectedIndex = 0;
  }
  return success;
  }
  private void TimeComboBox_VisibleChanged(object sender, EventArgs e)
  {
  if (!_shown)
  {
    SetHour();
    _shown = true;
  }
  }
 
  internal class Editor : UITypeEditor
  {
  private IWindowsFormsEditorService _svc;
  public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
  {
    return UITypeEditorEditStyle.DropDown;
  }
  public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
  {
    _svc = (IWindowsFormsEditorService)provider.GetService(typeof(IWindowsFormsEditorService));
 
    var listBox = new ListBox();
    var hours = new Hours();
    var data = hours.Range(ParentIncrement);
 
    foreach (var item in data)
    {
    listBox.Items.Add(item);
    }
 
    if (value != null)
    {
    listBox.SelectedItem = value;
    }
 
    _svc.DropDownControl(listBox);
 
    value = (string)listBox.SelectedItem;
 
    return value;
  }
 
  }
}

DomainUpDown

Another option for presenting time only is with a standard DomainUpDown control which would be populated using the same method as the Time ComboBox presented above. In the code block below a standard DomainUpDown is being populated.
var hours = new Hours();
HoursDomainUpDown.Items.AddRange(hours.Range(TimeIncrement.Quarterly));

In the included code sample methods are shown to keep the DomainUpDown control synchronized in this case with a List<T> read from SQL-Server database table.

When dealing with a DomainUpDown control loaded with time values which are strings which represent a TimeSpan method are needed to work with these values as a TimeSpan. 

Extension method to parse a TimeSpan from a string that represents a valid TimeSpan.

public static TimeSpan TimeSpan(this string sender)
{
  var sb = new StringBuilder(sender);
  var result = sb.Replace(" AM", string.Empty).Replace(" PM", string.Empty).ToString();
 
  return DateTime.TryParseExact(result, "hh:mm",
  CultureInfo.InvariantCulture,DateTimeStyles.None, out var dt) ?
  dt.TimeOfDay : new TimeSpan();
}

Now the above extension is a good example of code which appears like it should work but has issues. Perhaps the cool factor blinded the developer as it's concise and compact. A better method is shown below.

public static TimeSpan TimeSpan(this string sender)
{
  var sb = new StringBuilder(sender);
  var result = sb.Replace(" AM", string.Empty).Replace(" PM", string.Empty).ToString();
  var tmp = result.Replace(":", "");
  if (tmp.IsNumeric())
  {
  try
  {
    return System.TimeSpan.ParseExact(result, "h\\:mm", CultureInfo.CurrentCulture, TimeSpanStyles.None);
 
  }
  catch (FormatException)
  {
    return new TimeSpan();
  }
  }
  else
  {
  return new TimeSpan();
  }
}

Use to convert the currently select time string in a DomainUpDown control.

TimeSpan selectedTime = HoursDomainUpDown.Text.TimeSpan();

Formatting can be done by chaining another extension which formats with AM/PM designators.

public static string Formatted(this TimeSpan sender, string format = "hh:mm tt")
{
  return DateTime.Today.Add(sender).ToString(format);
}

Example

Console.WriteLine($"DomainUpDown: {HoursDomainUpDown.Text.TimeSpan().Formatted()}");

Dealing with start and ending times

Common rules would be that a end time may not be less than the selected start time and start time may not be greater than end time.  Dependent on the control used the following extension methods will assist to determine if a value is out of range.
/// <summary>
/// Is end time prior to start time
/// </summary>
/// <param name="endTimeSpan"></param>
/// <param name="startTimeSpan"></param>
/// <returns></returns>
public static bool IsValidEndTime(this TimeSpan endTimeSpan, TimeSpan startTimeSpan)
{
  return endTimeSpan.Hours < startTimeSpan.Hours;
}
/// <summary>
/// Is start time after end time
/// </summary>
/// <param name="startTimeSpan"></param>
/// <param name="endTimeSpan"></param>
/// <returns></returns>
public static bool IsValidStartTime(this TimeSpan startTimeSpan, TimeSpan endTimeSpan)
{
  return endTimeSpan.Hours > startTimeSpan.Hours;
}

Time is in range

Another consideration is a selected time within a specific range when a control housing time does not have max and min values. The following generic language extension handles this requirement.
public static class GenericExtensions
{
  public static bool Between<T>(this T actual, T lower, T upper) where T : IComparable<T>
  {
  return actual.CompareTo(lower) >= 0 && actual.CompareTo(upper) < 0;
  }
}

Example where 8:15 is between 8:00 and 8:30.

var startTimeSpan = new TimeSpan(8, 00, 0);
var testTimeSpan = new TimeSpan(8,15,0);
var endTimeSpan = new TimeSpan(8, 30, 0);
 
if (testTimeSpan.Between(startTimeSpan,endTimeSpan))
{
  Console.WriteLine("Yes");
}
else
{
  Console.WriteLine("No");
}

References

Running code samples

In Script project, open CreateDatabaseAndPopulate script in Visual Studio or SSMS (SQL-Server Management Studio) and execute the scripts to create the database, create two tables and populate both tables.

In the project BaseLibrary, class BaseSqlServerConnections change the property DatabaseServer from KARENS-PC to either .\SQLEXPRESS for express edition of SQL-Server or a named instance for a full edition of SQL-Server, otherwise a controlled exception will be raised informing you the property needs to change for your edition of SQL-Server.

There are three forms in the main project (WIndowsFormsApp1) where Form1 is setup as the main form. Compile and run, work through working with the custom ComboBox, DomainUpDown and DateTimePicker which work against SQL-Server data. While doing so traverse each record in the DataGridView (which has been setup to appear like a ListView) and note beneath the DataGridView there is a label which indicates if the end time is invalid against the start time e.g. end time is before start time. Once running the code review the underlying source code and set breakpoints to learn how everything works under the covers including various language extension methods.

Next in Program.cs change the startup form to ConventionalForm, build-run. In the form data binding is done with a DateTime field bound to a DateTimePicker which works while as stated above if a time field was data bound changes would not be reflected properly.

Next in Program.cs change the startup for to DateTimePickerForm where in this case a time field is data bound to a DateTimePicker which relies on events to keep changes in the DateTimePicker in sync with the underlying DataTable which uses a TimeSpan instead of DateTime.

Take time to study the extension methods included in the source code in TimeLibrary.DateTimeExtensions. They can be used as is or copied into your project to use.

How to use in your projects

  • TimeLibrary (class project) contains a class to create a range of times along with a custom ComboBox. Add this project to your Visual Studio solution to utilize the ComboBox and class to generate time range. This library also contains many useful language extensions for TimeSpan and DateTime conversions and formatting.
  • Add a reference to your front end project for TimeLibrary.
  • Build your solution.
  • A new control, TimeComboBox will be added to your Visual Studio toolbox which can be used by following code samples in the source code provided with this article.

Summary

In this article the basics of understanding SQL-Server time field and receiving a time field data in a Visual Studio project along with obtaining time from SQL-Server datetime(7). Which field type to use is usually datetime(7) yet when date information is not required a time field fits in nicely. 

Requires

See also

Source code

https://github.com/karenpayneoregon/WorkingWithTimeInVisualStudio