4

I've created a simple CompositeControl and exposed a Nullable DateTimeOffset property. I'm binding the control to a SQL Server DateTimeOffset field using

DateTimeOffset='<%# Bind("myDateTimeOffsetField") %>'

This works great when the DateTimeOffset field has a value. But when the field is NULL I get a "Specified Cast is not valid" error.

How do I stop this error and set my property to Nothing when the field is NULL?

I thought this would be the default behaviour!

Property definition is:

Public Property DateTimeOffset As DateTimeOffset?

Later comment:

I've found that this works if I change from using Bind to:

DateTimeOffset='<%# iif(IsDbNull(Eval("myDateTimeOffsetField")), Nothing, Eval("myDateTimeOffsetField")) %>'

But then I don't get "myDateTimeOffsetField" passed as an argument in the FormView.ItemUpdating event (yes, this is in a FormView control) since ASP.NET assumes I'm not binding back to the Database.

Actual Code (Added by Request)

This is the Property in my Composite Control that I'm trying to bind to:

Public Property DateTimeOffset As DateTimeOffset?
Get
    Return CType(ViewState("DTO"), DateTimeOffset?)
End Get
Set(value As DateTimeOffset?)
    ViewState("DTO") = value
End Set
End Property

Heres the markup for the Binding. The Control is in the EditItemTemplate of a FormView which is bound to a SQL DataSource returning a field called [dtoMldRejOn] with an optional DateTimeOffset value.

<APS:DateTimeOffsetControl runat="server" id="dtocMldRejOn" TextBoxCssClass="inputdatetime" ValidationGroup="vw1" FieldName="<%$ Resources: Resource, rxgFrom %>" DateTimeOffset='<%# Bind("dtoMldRejOn") %>' WindowsTimeZoneID="<%# me.WindowsTimeZoneID %>" IsRequired="false" />

As you can see, my Composite control is for handling DateTimeOffset values. It all works great unless the DateTimeOffset field [dtoMldRejOn] from the database is NULL, then I get the exception.

PapillonUK
  • 642
  • 8
  • 20

5 Answers5

1

I have never created bindable controls before, but I would like to make suggestion. How about setting your DateTimeOffset property to be of type Object. That way, the property will accept any data types including DBNull.

And once inside the Set code, check if the value passed is DBNull.Value. If so, create a new empty DataTimeOffset? object and save it in the ViewState.

If non DBNull values, throw error if it cannot be be converted to datetime.

I didn't try this though so I don't know if this will work or not.

################ UPDATED ANSWER ################

My suggestion is, you create 2 properties as follows:

Public Property DateTimeOffset() As DateTimeOffset?
    Get
        Return DirectCast(ViewState("DTO"), DateTimeOffset?)
    End Get
    Set(ByVal Value As DateTimeOffset?)
        ViewState("DTO") = Value
    End Set
End Property

<Bindable(True, BindingDirection.TwoWay)>
Public Property DbDateTimeOffset As Object
    Get
        Return Me.DateTimeOffset
    End Get
    Set(value As Object)
        If IsDBNull(value) OrElse value Is Nothing Then
            Me.DateTimeOffset = New DateTimeOffset?
        Else
            Me.DateTimeOffset = DirectCast(value, DateTimeOffset?)
        End If
    End Set
End Property

So in your markup, the binding will be to the DbDateTimeOffset property:

DbDateTimeOffset='<%# Bind("myDateTimeOffsetField") %>'

While in code behind, you can use the other property to read the property without having to cast.

ajakblackgoat
  • 2,119
  • 1
  • 15
  • 10
  • Hi, yes I've already tried that and it works. I check for IsDbNull in the object property setter and if True I then set the "real" DateTimeOffset property to Nothing. This all works but unfortunately it means I always have to cast the object property to a nullable DateTimeOffset whenever I read it back out - or else always read from the "real" property and write to the "object" property. I'm not that happy with this solution though as I'm designing the control for others to use and I'd rather avoid this complication if possible. This may be the best workaround if I can't find a solution. – PapillonUK May 04 '13 at 21:23
  • Comment on your UPDATED ANSWER: Thanks ajakblackgoat. I don't like this messy Object idea (!), but like you i've come to the same verdict - looks like i'll have to go for it until something better comes along. I'll accept your answer since its neater than my own implementation and the bounty is about to expire! Thanks again. – PapillonUK May 09 '13 at 13:22
  • Crazy, really that the only purpose being served here is to check for DbNull in the property Getter and convert it to Nothing if True! I suspect there's a better way but this will do for now. Note that in the other direction a DateTimeOffset? of Nothing converts automatically to a NULL without issue! – PapillonUK May 09 '13 at 13:41
  • Well thanks for the points. For your information, although the idea is messy, but that's how one (at least) of the most popular commercial datepicker control is doing it. – ajakblackgoat May 12 '13 at 21:14
  • Ha - interesting to know ajak! Thanks again, points well deserved. – PapillonUK May 17 '13 at 09:06
0

Based on this post,

I think you just need to mark your property with the Bindable attribute:

<System.ComponentModel.Bindable(True)> _
Public Property DateTimeOffset As DateTimeOffset?
Matt Johnson-Pint
  • 230,703
  • 74
  • 448
  • 575
  • Dang, I thought you had it then when I read that post, but no, exactly the same error "Specified Cast is not Valid" occurs in the CompositeControl.DataBind(). I'm wondering if ASP.NET does not know how to bind a NULL DateTimeOffset – PapillonUK May 03 '13 at 22:06
  • @PapillonUK - It's not DTO that is the problem. It's just binding a nullable value that's the issue. It has to understand that `DBNull.Value` is the same as null (`Nothing` in VB). It should just work. Can you update your answer and post more of your code so I can just run exactly what you are running? – Matt Johnson-Pint May 03 '13 at 22:17
  • I get the exact same issue if I make my DateTimeOffset property non-nullable. I really think it's the DateTimeOffset it's struggling with. I'll try and put some code up! – PapillonUK May 03 '13 at 22:46
0

The problem is that DbNull is different from Nothing and you must explicitly write that somewhere in your code. My first idea here was to use the binding events to add the value. So if you keep you code like this:

DateTimeOffset='<%# iif(IsDbNull(Eval("myDateTimeOffsetField")), Nothing, Eval("myDateTimeOffsetField")) %>'

You can manually add the DateTimeOffset parameter in the Updating events before the binding proceeds (I can update the answer later with more details on this if you want)

Anyway, after reading your code more carefully I thought that maybe the CType isn't casting correctly. Have you tried replacing your Get with something like this?

Get
    Return If(IsDbNull(ViewState("DTO")), Nothing, CType(ViewState("DTO"), DateTimeOffset?))
End Get
nmat
  • 7,430
  • 6
  • 30
  • 43
  • The change to the Get command results in the same "cast is not valid error" during the initial bind so I suspect the error is during the Set rather than the get. Yes I know I can add the parameter during the Updating event using FindControl for example. This is infact what I'm doing, but I don't like it as the control is for use by other people too! Thanks for the thought though! – PapillonUK May 09 '13 at 13:12
  • @PapillonUK If it's on the set, do it the other way around. `If(string.IsNullOrEmpty(value))) Then ViewState("DTO") = DbNull.Value` – nmat May 09 '13 at 13:20
  • Hi nmat - The code to test for DbNull in the Getter would never be hit. The cast exception occurs before the Getter is even called and seems to be due to the fact that .NET cannot handle the conversion of a DbNull to a DateTimeOffset during data-binding. – PapillonUK May 09 '13 at 22:53
0

Your code with iif ... works for me.
I have created a testing control and a page version with code behind pages ( I have tested code behind version too but this one is easier to publish). I have VS2010 target framework is 4.0. First the control(TstNullableCtrl.ascx):

<%@ Control Language="vb" AutoEventWireup="false" %>
<script runat="server">
    Public Property DateTimeOffset As DateTimeOffset?
</script>
<div ID="Label1" runat="server" >
 <%= If(DateTimeOffset.HasValue, DateTimeOffset.ToString, "Empty")%>
</div>

and the page - a table with two rows and two columns bound to datagrid (TstNullablePage.aspx):

<%@ Page Language="vb" AutoEventWireup="false"  %>
<%@ Import Namespace="System.Data"%>
<%@ Register src="TstNullableCtrl.ascx" tagname="TstNullableCtrl" tagprefix="uc1" %>
<script runat="server">
    Protected Sub Page_Load(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Load
        Dim tbl As New DataTable
        tbl.Columns.Add(New DataColumn("id", GetType(Integer)) With {.AutoIncrement = True})
        tbl.Columns.Add(New DataColumn("myDateTimeOffsetField", GetType(DateTimeOffset)))
        Dim row As DataRow
        row = tbl.NewRow : row("myDateTimeOffsetField") = DateTimeOffset.Now
        tbl.Rows.Add(row)
        row = tbl.NewRow : row("myDateTimeOffsetField") = DBNull.Value
        tbl.Rows.Add(row)
        tstgrd.DataSource = tbl : tstgrd.DataBind()
    End Sub
</script>
<html>
<body>
    <form id="form1" runat="server">
      <asp:datagrid ID="tstgrd" runat="server">
        <Columns>
         <asp:TemplateColumn HeaderText="Offset">
           <itemtemplate>    
             <uc1:TstNullableCtrl ID="WithNullableDate1" runat="server" 
               DateTimeOffset='<%# iif(IsDbNull(Eval("myDateTimeOffsetField")), Nothing, Eval("myDateTimeOffsetField")) %>' />
            </itemtemplate>
         </asp:TemplateColumn>
         </Columns>
        </asp:datagrid>
    </form>
</body>
</html>

And expected result (a value when there is and 'Empty' when is null)

result

Edit

However I think that to "avoid complication" best solution is as follows:

Public _DateTimeOffset As Object
Public Property DateTimeOffset As Object
Get
    If IsDBnull(_DateTimeOffset) then Return Nothing
    Return Ctype(_DateTimeOffset, DateTimeOffset?) 
End Get
Set(value As Object)
    _DateTimeOffset = value
End Set
End Property 
IvanH
  • 5,039
  • 14
  • 60
  • 81
  • Thanks for that. Yes, complication is what I'm trying to avoid and you've come to the same solution - i.e. I have to use an Object for the property rather than a DateTimeOffset as ASP.NET has trouble binding a dbNull to a DateTimeOffset structure. I think ajakblackgoat has provided the fullest solution for this workaround so I appreciate the work here but i'll have to give him the points! – PapillonUK May 09 '13 at 13:19
0

I know this question have been already answered I just would like to document my solution which is in between those two answers as I have came across this today:

Private _AssetId As Object
<Bindable(True, BindingDirection.TwoWay)>
Public Property AssetId() As Object
    Get
        If _AssetId Is Nothing Then                
             Return Nothing                
        Else
            Return CType(_AssetId, Integer)
        End If
    End Get
    Set(ByVal value As Object)
        If value Is Nothing OrElse IsDBNull(value) OrElse CType(value, String) = "" Then
            _AssetId = Nothing                
        Else
            _AssetId = CType(value, Integer)                
        End If
    End Set
End Property
Vivi Rosa
  • 1
  • 1