Solved

DateTimePicker value changes after validation

Posted on 2006-11-17
6
3,605 Views
Last Modified: 2011-08-18
I've discovered an odd behavior of the DateTimePicker which I frankly think is a bug in the control. I'm working in VB 2003, but I've verified this happens in VB 2005 as well. If you enter a single digit for a day or month, and don't change anything else in the date, the ValueChanged event doesn't fire until after the Validating and Validated events. I can understand why the event doesn't fire as soon as the digit is entered, since one might be entering two digits. But the fact that it doesn't fire until after the Validating event means there is no way to validate such input--when Validating fires, the old value is still present as the .Value property.

The only way I can find to have a subroutine run after the value has truly changed is to put something in LostFocus. That's workable for some of my needs. However, I seem to be out of luck in one use: putting a custom DateTimePicker control into a DataGridColumnStyle. I'm more or less using Microsoft's example from http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpref/html/frlrfsystemwindowsformsdatagridcolumnstyleclasstopic.asp, which for everything else seems to be doing a good job. But if a single-digit day or month is entered and then the user leaves the DTP, the change is lost. (The value isn't changed before the Commit method fires.) Further experimenting indicates that if one types either a leading zero or a trailing slash (to make a two-digit day/month or to indicate the end of the day/month and moving on to the next part of the date), that will fire the ValueChanged event. Is my only solution user education to do one of those things, or is there another way to get the value changed before Validating/Commit fires?
0
Comment
Question by:ElrondCT
  • 4
  • 2
6 Comments
 
LVL 34

Expert Comment

by:Sancler
ID: 17971886
Try using a custom format with a single marker for the day and the month.  Like "d M yy" (UK) or "M d yy" (US).  For me, that fires value changed with a single digit entry before validation fires.  I haven't tested it with a DateTimePicker in a DataGridColumnStyle, but I can't offhand think why it should be different.  It might, at least, be worth a try.

Roger
0
 
LVL 34

Expert Comment

by:Sancler
ID: 17972294
On second thoughts, I don't think the behaviour with a custom format is any different from with any other.  I think, rather, I may have missed your point.  So ignore that.  I'll try and look at it some more a bit later.

Roger
0
 
LVL 34

Expert Comment

by:Sancler
ID: 17976484
I now see what you mean.  I agree that it's probably a bug (or a pretty radical design flaw).  

The control is, I reckon, composed of various sub-controls.  Using the analogy of a datagrid's rows, the movement from cell to cell within a row "commits" an edit to the cell that has just been left, and any move to a new row both "commits" the cell that has just been left and then "commits" the whole row that has just been left.  In the analogous DTP situation cell-level committal seems to occur either when a cell is "full" (i.e. 2 digits in a cell which will take 2 digits) or when a new cell is moved to (e.g. from day to month, or from a value cell to a place holder cell) and committal of the whole thing occurs when focus moves out of the control.  But the committal of the current cell is not forced before the committal of the whole thing so, if the cell concerned was not "full", the change to it happens too late for the full committal.

This means that there does not appear to be any workaround for it at the level of the DTP control itself.  As it's a "black box", with this odd behaviour built-in, all my attempts to catch and alter or redirect the user actions that might call what I've referred to as the full committal have failed.  I've even tried customising it - overriding some of its methods - again with no joy.

You've found a workaround - using LostFocus - when using the DTP as a stand alone control.  From my trials, I don't think you'll better that.  In the context of a custom column in a datagrid, about the only alternative I can suggest to "user education" is to shift data validation from the cell level to the row level (i.e. when current row changes so that the whole record would be sent to the datatable, or before an .EndCurrentEdit is called), or at least to some "next cell" code.  What I mean by the latter is something on these lines.  In the datagrid's CurrentCellChangedEvent (pseudo code)

    Static CheckDate As Boolean = False
    If CheckDate Then
        'go back and check the date in the cell just left
    End If
    If currentcolumn is datecolumn then
        CheckDate = True
    Else
        CheckDate = False
    End If

Roger
0
How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

 
LVL 20

Author Comment

by:ElrondCT
ID: 17979822
Thanks for your testing, Roger. Unfortunately, your idea doesn't help for a custom column in a datagrid; in that situation, the changed information doesn't get saved at all. If you enter a single-digit day or month, and don't close it out by switching to another "cell" (month, day, year inside the DTP control) either by clicking or by hitting the slash, the single-digit entry is completely lost; when you leave the DTP, the date displayed in the datagrid is what was shown before you entered the single-digit day/month. Since it's a separate control feeding the datagrid, waiting for data validation doesn't help, as the wrong data is getting fed to the datagrid.

I think probably the only real solution would be to recode the DTP as a class. That's beyond what I'm willing to do at this point, so for now I'll rely on user education, and anticipate a few unhappy phone calls coming down the pike. I'm hoping that such data entry won't happen very often anyway--that people will be more likely to use + and -, or also enter a year (which would trigger the ValueChanged event).

I'm frankly surprised that the bug persists in VB 2005. I would have thought that over the 3 years or so since VB .Net first came out, someone would have noticed and said, "We need to correct this." I'm not sure of how to place a bug report wtih MS without paying for it (or using up a limited number of free support requests). The information I see Microsoft's web site on support requests doesn't say anything about "no charge if due to a bug in Microsoft software;" I guess they're big enough they think they can get away with that.
0
 
LVL 34

Accepted Solution

by:
Sancler earned 500 total points
ID: 17981783
Try this

Imports System
Imports System.Data
Imports System.Windows.Forms
Imports System.Drawing
Imports System.ComponentModel

' This example shows how to create your own column style that
' hosts a control, in this case, a DateTimePicker.
Public Class DataGridTimePickerColumn
    Inherits DataGridColumnStyle

    'DECLARATION CHANGED TO WithEvents
    Private WithEvents customDateTimePicker1 As New CustomDateTimePicker()
    'NEW DECLARATIONS
    Private thisCM As CurrencyManager
    Private thisRow As Integer

    ' The isEditing field tracks whether or not the user is
    ' editing data with the hosted control.
    Private isEditing As Boolean

    Public Sub New()
        customDateTimePicker1.Visible = False
    End Sub

    Protected Overrides Sub Abort(ByVal rowNum As Integer)
        isEditing = False
        RemoveHandler customDateTimePicker1.ValueChanged, _
            AddressOf TimePickerValueChanged
        Invalidate()
    End Sub

    Protected Overrides Function Commit _
        (ByVal dataSource As CurrencyManager, ByVal rowNum As Integer) _
        As Boolean

        'TWO NEW LINES
        thisCM = dataSource
        thisRow = rowNum
        customDateTimePicker1.Bounds = Rectangle.Empty

        RemoveHandler customDateTimePicker1.ValueChanged, _
            AddressOf TimePickerValueChanged

        If Not isEditing Then
            Return True
        End If
        isEditing = False



        Try
            Dim value As DateTime = customDateTimePicker1.Value
            SetColumnValueAtRow(dataSource, rowNum, value)
        Catch
        End Try
        Invalidate()

        Return True
    End Function

    Function reCommit(ByVal dateVal As Date) _
        As Boolean
        customDateTimePicker1.Bounds = Rectangle.Empty

        RemoveHandler customDateTimePicker1.ValueChanged, _
            AddressOf TimePickerValueChanged

        isEditing = False

        Try
            Dim value As DateTime = dateVal
            SetColumnValueAtRow(thisCM, thisRow, value)
        Catch
        End Try
        Invalidate()

        Return True

    End Function
    Protected Overloads Overrides Sub Edit( _
        ByVal [source] As CurrencyManager, _
        ByVal rowNum As Integer, _
        ByVal bounds As Rectangle, _
        ByVal [readOnly] As Boolean, _
        ByVal displayText As String, _
        ByVal cellIsVisible As Boolean)

        Dim value As DateTime = _
        CType(GetColumnValueAtRow([source], rowNum), DateTime)
        If cellIsVisible Then
            customDateTimePicker1.Bounds = New Rectangle _
            (bounds.X + 2, bounds.Y + 2, bounds.Width - 4, _
            bounds.Height - 4)

            customDateTimePicker1.Value = value
            customDateTimePicker1.Visible = True
            AddHandler customDateTimePicker1.ValueChanged, _
            AddressOf TimePickerValueChanged
        Else
            customDateTimePicker1.Value = value
            customDateTimePicker1.Visible = False
        End If

        If customDateTimePicker1.Visible Then
            DataGridTableStyle.DataGrid.Invalidate(bounds)
        End If

        customDateTimePicker1.Focus()

    End Sub

    Protected Overrides Function GetPreferredSize( _
        ByVal g As Graphics, _
        ByVal value As Object) As Size

        Return New Size(100, customDateTimePicker1.PreferredHeight + 4)

    End Function

    Protected Overrides Function GetMinimumHeight() As Integer
        Return customDateTimePicker1.PreferredHeight + 4
    End Function

    Protected Overrides Function GetPreferredHeight( _
        ByVal g As Graphics, ByVal value As Object) As Integer

        Return customDateTimePicker1.PreferredHeight + 4

    End Function

    Protected Overloads Overrides Sub Paint( _
        ByVal g As Graphics, ByVal bounds As Rectangle, _
        ByVal [source] As CurrencyManager, ByVal rowNum As Integer)

        Paint(g, bounds, [source], rowNum, False)

    End Sub

    Protected Overloads Overrides Sub Paint(ByVal g As Graphics, _
        ByVal bounds As Rectangle, ByVal [source] As CurrencyManager, _
        ByVal rowNum As Integer, ByVal alignToRight As Boolean)

        Paint(g, bounds, [source], rowNum, Brushes.Red, _
            Brushes.Blue, alignToRight)

    End Sub

    Protected Overloads Overrides Sub Paint(ByVal g As Graphics, _
        ByVal bounds As Rectangle, ByVal [source] As CurrencyManager, _
        ByVal rowNum As Integer, ByVal backBrush As Brush, _
        ByVal foreBrush As Brush, ByVal alignToRight As Boolean)

        Dim [date] As DateTime = _
        CType(GetColumnValueAtRow([source], rowNum), DateTime)
        Dim rect As Rectangle = bounds
        g.FillRectangle(backBrush, rect)
        rect.Offset(0, 2)
        rect.Height -= 2
        g.DrawString([date].ToString("d"), _
            Me.DataGridTableStyle.DataGrid.Font, foreBrush, _
            RectangleF.FromLTRB(rect.X, rect.Y, rect.Right, rect.Bottom))

    End Sub

    Protected Overrides Sub SetDataGridInColumn(ByVal value As DataGrid)
        MyBase.SetDataGridInColumn(value)
        If Not (customDateTimePicker1.Parent Is Nothing) Then
            customDateTimePicker1.Parent.Controls.Remove(customDateTimePicker1)
        End If
        If Not (value Is Nothing) Then
            value.Controls.Add(customDateTimePicker1)
        End If
    End Sub

    Private Sub TimePickerValueChanged( _
        ByVal sender As Object, ByVal e As EventArgs)

        ' Remove the handler to prevent it from being called twice in a row.
        RemoveHandler customDateTimePicker1.ValueChanged, _
            AddressOf TimePickerValueChanged
        Me.isEditing = True
        MyBase.ColumnStartedEditing(customDateTimePicker1)

    End Sub

    Private Sub customDateTimePicker1_NewDate(ByVal dateval As Date) Handles customDateTimePicker1.NewDate
        reCommit(dateval)
    End Sub
End Class

Public Class CustomDateTimePicker
    Inherits DateTimePicker

    Public Event NewDate(ByVal dateval As DateTime)


    Protected Overrides Function ProcessKeyMessage(ByRef m As Message) As Boolean
        ' Keep all the keys for the DateTimePicker.
        Return ProcessKeyEventArgs(m)
    End Function

    Private Sub CustomDateTimePicker_Validating(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs) Handles Me.Validating
        RaiseEvent NewDate(MyBase.Value)
    End Sub

End Class

Public Class MyForm
    Inherits Form

    Private namesDataTable As DataTable
    Private WithEvents myGrid As DataGrid = New DataGrid()

    Public Sub New()

        InitForm()

        namesDataTable = New DataTable("NamesTable")
        namesDataTable.Columns.Add(New DataColumn("Name"))
        Dim dateColumn As DataColumn = _
             New DataColumn("Date", GetType(DateTime))
        dateColumn.DefaultValue = DateTime.Today
        namesDataTable.Columns.Add(dateColumn)
        Dim namesDataSet As DataSet = New DataSet()
        namesDataSet.Tables.Add(namesDataTable)
        myGrid.DataSource = namesDataSet
        myGrid.DataMember = "NamesTable"
        AddGridStyle()
        AddData()

    End Sub

    Private Sub AddGridStyle()
        Dim myGridStyle As DataGridTableStyle = _
                    New DataGridTableStyle()
        myGridStyle.MappingName = "NamesTable"

        Dim nameColumnStyle As DataGridTextBoxColumn = _
            New DataGridTextBoxColumn()
        nameColumnStyle.MappingName = "Name"
        nameColumnStyle.HeaderText = "Name"
        myGridStyle.GridColumnStyles.Add(nameColumnStyle)

        Dim customDateTimePicker1ColumnStyle As DataGridTimePickerColumn = _
            New DataGridTimePickerColumn()
        customDateTimePicker1ColumnStyle.MappingName = "Date"
        customDateTimePicker1ColumnStyle.HeaderText = "Date"
        customDateTimePicker1ColumnStyle.Width = 100
        myGridStyle.GridColumnStyles.Add(customDateTimePicker1ColumnStyle)

        myGrid.TableStyles.Add(myGridStyle)
    End Sub

    Private Sub AddData()
        Dim dRow As DataRow = namesDataTable.NewRow()
        dRow("Name") = "Name 1"
        dRow("Date") = New DateTime(2001, 12, 1)
        namesDataTable.Rows.Add(dRow)

        dRow = namesDataTable.NewRow()
        dRow("Name") = "Name 2"
        dRow("Date") = New DateTime(2001, 12, 4)
        namesDataTable.Rows.Add(dRow)

        dRow = namesDataTable.NewRow()
        dRow("Name") = "Name 3"
        dRow("Date") = New DateTime(2001, 12, 29)
        namesDataTable.Rows.Add(dRow)

        dRow = namesDataTable.NewRow()
        dRow("Name") = "Name 4"
        dRow("Date") = New DateTime(2001, 12, 13)
        namesDataTable.Rows.Add(dRow)

        dRow = namesDataTable.NewRow()
        dRow("Name") = "Name 5"
        dRow("Date") = New DateTime(2001, 12, 21)
        namesDataTable.Rows.Add(dRow)

        namesDataTable.AcceptChanges()
    End Sub

    Private Sub InitForm()
        Me.Size = New Size(500, 500)
        myGrid.Size = New Size(350, 250)
        myGrid.TabStop = True
        myGrid.TabIndex = 1
        Me.StartPosition = FormStartPosition.CenterScreen
        Me.Controls.Add(myGrid)
    End Sub

    <STAThread()> _
    Public Shared Sub Main()
        Application.Run(New MyForm())
    End Sub


End Class

It's the example from the link you provided, with a few alterations.  I think I've commented them all.

The idea is to pick up the fact that the normal DTP shows the "right" value when its Validating sub is called.  So an event is added to the basic DTP in this, and the event is raised, with the "right" value passed as its argument, in the Validating sub.

The custom column has code to deal with that event: it calls a new sub - reCommit - whcih is a modified version of the original Commit sub.  So that that can operate on the right objects, there are two new variables declared to which the existing Commit sub passes its arguments.  I don't doubt it could be streamlined.  I imagine also that, as your existing code is only "based on" this example, it might need tinkering with for your purposes.  But it seems to work OK for me.

Roger
0
 
LVL 20

Author Comment

by:ElrondCT
ID: 17982024
I'm impressed once again, Roger. I did have to hunt through the code a bit to find some of your changes (the new subs weren't flagged), and it needed MyBase.Validating rather than Me.Validating, but that was no big deal. This seems to work properly. I was going to tell you that the DTP doesn't show the right value when Validating is called, but for whatever reason, in this context it works. (According to breakpoints I set, with this code Validating fires after Lostfocus.)

The primary change I had made to the code MS provided was to put in KeyDown/KeyPress code to process Quicken-style date shortcuts (Y and R for begin and end of YeaR, etc.). Your changes don't affect that at all, except for needing to remove the handlers in the reCommit code, which is trivial. Thanks for your effort.
0

Featured Post

What Should I Do With This Threat Intelligence?

Are you wondering if you actually need threat intelligence? The answer is yes. We explain the basics for creating useful threat intelligence.

Join & Write a Comment

Suggested Solutions

I think the Typed DataTable and Typed DataSet are very good options when working with data, but I don't like auto-generated code. First, I create an Abstract Class for my DataTables Common Code.  This class Inherits from DataTable. Also, it can …
1.0 - Introduction Converting Visual Basic 6.0 (VB6) to Visual Basic 2008+ (VB.NET). If ever there was a subject full of murkiness and bad decisions, it is this one!   The first problem seems to be that people considering this task of converting…
Excel styles will make formatting consistent and let you apply and change formatting faster. In this tutorial, you'll learn how to use Excel's built-in styles, how to modify styles, and how to create your own. You'll also learn how to use your custo…
This tutorial demonstrates a quick way of adding group price to multiple Magento products.

759 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

19 Experts available now in Live!

Get 1:1 Help Now