thman
asked on
Binding lost after refreshing
Why do my bound controls lose binding relation to my data source control after the data source control?s recordset refreshed?
In my project, I need my own data source control instead of data control provided by MS ADO to provide special business rules control. In my test usercontrol (looks exactly like the adodc control), I successfully got my recordset bound to appropriate data consumers, among which there?s a datagrid. It looks everything is fine when I move from record to record, or do addnew, update, begintrans, committrans and rollbacktrans. But there?s a very ugly problem. When my operation to the data source involves refreshing (rs.close and rs.open), the bound controls will lose binding relation to the data source and they will act on themselves with the old recordset no matter how the data source is doing. When the datagrid moves from row to row, other bound controls will change respectively. From the debug messages printed in the data source?s events, I know the data source control itself works very normally except not controlling the bound controls any more.
I guess I need to do something like updatecontrols in an appropriate event to inform the bound controls that I have refreshed the recordset. But I don?t know how to do it. I don?t know if there?s any other way than getdatamember that can establish or reestablish the relation between a data source and a data consumer. I also consider it?s very ugly if I have to programmatically bind data consumers one by one. I hope there?s some convenient way to tell the data consumers themselves to do the binding again.
So please teach me why the binding is lost and how to resolve it.
Thanks in advance.
Thman
In my project, I need my own data source control instead of data control provided by MS ADO to provide special business rules control. In my test usercontrol (looks exactly like the adodc control), I successfully got my recordset bound to appropriate data consumers, among which there?s a datagrid. It looks everything is fine when I move from record to record, or do addnew, update, begintrans, committrans and rollbacktrans. But there?s a very ugly problem. When my operation to the data source involves refreshing (rs.close and rs.open), the bound controls will lose binding relation to the data source and they will act on themselves with the old recordset no matter how the data source is doing. When the datagrid moves from row to row, other bound controls will change respectively. From the debug messages printed in the data source?s events, I know the data source control itself works very normally except not controlling the bound controls any more.
I guess I need to do something like updatecontrols in an appropriate event to inform the bound controls that I have refreshed the recordset. But I don?t know how to do it. I don?t know if there?s any other way than getdatamember that can establish or reestablish the relation between a data source and a data consumer. I also consider it?s very ugly if I have to programmatically bind data consumers one by one. I hope there?s some convenient way to tell the data consumers themselves to do the binding again.
So please teach me why the binding is lost and how to resolve it.
Thanks in advance.
Thman
Hi thman,
please show your codes so ppls can help
regards.
please show your codes so ppls can help
regards.
ASKER
Hi, Ryancys,
My test codes are very similar to the sample of the AxData come with the VS6.0. I haven?t added any business rules into it yet. So my problem exists in the sample too.
Hi, Bahnass,
If I was as smart as you to use the tag property, I shouldn?t have spent so much time to do the things described bellow. But when I received your hint, I had already started to work on the thoughts and couldn?t stop. Any way, I figured out a way that will not require users to make a tag while they bind a data consumer control to my data source component. It?s very tedious, though. Please keep me posted if you get any new idea. Thanks.
-----
I have a bit study on data consumer controls? DataSource property. It?s usually a string (some like datagrids are not) at design time and object at run time. When it?s an object, it?s still very special that you can know its type is the type of the data source bound to the consumer but you just can?t get access to any members of the kind of object. That is a problem if you have a few instance of that kind of data source because you will not be able to tell which exactly the data consumer is being bound to. Let?s say we have a data source control typed MyAdodc and have inserted to a form three instances of it, named MyAdodc1, MyAdodc2 and MyAdodc3 respectively. We bound them relatively to three groups of data consumers: (Datagrid1, Text1, Text2), (Datagrid2, Text3, Text4) and (Datagrid3, Text5, Text6). Each of these consumers? Data Source property is an object of MyAdodc type, we are not sure which instance it is bound to.
My solution is to try to save binding relation as a property at design time so we can get the information that helps us when rebinding at run time. Since the DataSource property is a string that contains the data source instance?s name, we can save this information to a private property (call CollectDataConsumer method) of MyAdodc when WriteProperties event rises. I tried to save them to a collection property and failed because I got an error message saying, ?object can?t save because it doesn?t support persistence?. So I have to save them as a string and then parse it to a collection when ReadProoerties event rises. To make sure we always get the property saved, we also need to raise a PropertyChanged event when getDataMember method is called. When users refresh the recordset, we need call the RebindDataConsumer method. Below are parts of the codes:
Withevents rs As adodb.Recordset
Private m_Ctls As Collection
Private m_strCtls As String
Private ctl As Control
Private myself As Object
Private isInRequery As Boolean
Private dontRaiseEvent As Boolean
'Events statements omitted.
Private Sub parseCtls()
Dim tmpstr As String
Dim tmpstrCtls As String
Dim tmplen As Integer
If Len(m_strCtls) = 0 Then
Set m_Ctls = Nothing
Exit Sub
Else
Set m_Ctls = New Collection
End If
tmpstrCtls = m_strCtls
tmplen = InStr(1, tmpstrCtls, ";")
Do While tmplen > 1
tmpstr = Mid(tmpstrCtls, 1, tmplen - 1)
m_Ctls.Add tmpstr, tmpstr
tmpstrCtls = Mid(tmpstrCtls, tmplen + 1, Len(tmpstrCtls) - tmplen)
tmplen = InStr(1, tmpstrCtls, ";")
Loop
If Len(tmpstrCtls) > 0 Then
m_Ctls.Add tmpstrCtls, tmpstrCtls
End If
If m_Ctls.Count = 0 Then Set m_Ctls = Nothing
End Sub
Private Sub CollectDataConsumers()
Dim tmpstr As String
Dim tmpdb As DataBinding
'Clear the string because we want to do it all over
m_strCtls = ""
For Each ctl In Extender.Parent.Controls
On Error Resume Next
'Get the type of the datasource property.
'if encountered an error, try next since it must not be a data consumer
tmpstr = TypeName(ctl.DataSource)
If Err.Number <> 0 Then
Err.Clear
Else
On Error GoTo 0
Debug.Print tmpstr & " " & ctl.Name
If tmpstr = "String" Then
'if the type is string, it contains the data source's instance name.
If ctl.DataSource = Extender.Name Then
m_strCtls = m_strCtls & IIf(Len(m_strCtls) = 0, "", ";") & ctl.Name
End If
Else
'Only when the type name is my type, it can be bound to me.
If tmpstr = TypeName(Me) Then
'still we need to check whether it is bound to exact me.
'At design time, the databindings collection includes bound data source information.
'At run time, we will lose the chance.
For Each tmpdb In ctl.DataBindings
Debug.Print tmpdb.PropertyName & ";" & tmpdb.DataSourceName & ";" & tmpdb.DataMember & ";" & tmpdb.DataField & ";IsDataSource:" & tmpdb.IsDataSource & ";IsBindable:" & tmpdb.IsBindable
If tmpdb.PropertyName = "DataSource" And tmpdb.DataSourceName = Extender.Name Then
m_strCtls = m_strCtls & IIf(Len(m_strCtls) = 0, "", ";") & ctl.Name
End If
Next
End If
End If
End If
Next
Debug.Print m_strCtls
End Sub
Private Sub RebindDataConsumers()
Dim tmpstr As String
'Don't want the user of this component to complain he gets too much events.
Dim isFirstBinding As Boolean
'Get the control of myself. It?s different from Me.
If myself Is Nothing Then
For Each ctl In Parent.Controls
If ctl.Name = Extender.Name Then
Set myself = ctl
Exit For
End If
Next
End If
isFirstBinding = True
For Each ctl In Parent.Controls
On Error Resume Next
tmpstr = TypeName(ctl.DataSource)
If Err.Number <> 0 Then
Err.Clear
Else
On Error GoTo 0
Debug.Print tmpstr & " " & ctl.Name
If tmpstr = "String" Then
'I don't think it could be a string.
'This is just for safe
If ctl.DataSource = Extender.Name Then
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
Else
If tmpstr = TypeName(Me) Then
'Initially, it goes here.
'we have to check whether it's in our list.
If isCtlIn(ctl) Then
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
End If
If tmpstr = Extender.Name Then
'Once we have set myself to the datasource property, it goes here.
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
End If
End If
Next
End Sub
Private Function isCtlIn(ByVal vCtl As Control) As Boolean
Dim tmpCtl As Variant
isCtlIn = False
If m_Ctls Is Nothing Then Exit Function
For Each tmpCtl In m_Ctls
If tmpCtl = vCtl.Name Then
isCtlIn = True
Exit For
End If
Next
End Function
Private Sub UserControl_GetDataMember( DataMember As String, Data As Object)
'If strCtls Is empty, set the property dirty
'So it will invoke WriteProperties to call CollectDataConsumers if this is at design time.
If Len(strCtls) = 0 Then strCtls = m_strCtls
'Omitted other codes.
End Sub
Private Sub UserControl_ReadProperties (PropBag As PropertyBag)
'Omitted other codes.
m_strCtls = PropBag.ReadProperty("strC tls", "")
parseCtls
End Sub
Private Sub UserControl_WritePropertie s(PropBag As PropertyBag)
'Omitted other codes.
CollectDataConsumers
strCtls = m_strCtls
Call PropBag.WriteProperty("str Ctls", m_strCtls, "")
End Sub
Private Sub rs_WillMove(ByVal adReason As EventReasonEnum, adStatus As EventStatusEnum, ByVal pRecordset As Recordset)
If Not dontRaiseEvent Then RaiseEvent WillMove(adReason, adStatus, pRecordset)
End Sub
Private Sub rs_MoveComplete(ByVal adReason As EventReasonEnum, ByVal pError As Error, adStatus As EventStatusEnum, ByVal pRecordset As Recordset)
If Not dontRaiseEvent Then RaiseEvent MoveComplete(adReason, pError, adStatus, pRecordset)
If adStatus = adStatusOK And isInRequery Then
'The undeniable move is done.
'Erase the mark and invoke the rebinding.
isInRequery = False
RebindDataConsumers
End If
If adStatus = adStatusOK And adReason = adRsnRequery Then
'Requery finished. It will take an undeniable move.
'So only make a mark here
isInRequery = True
End If
End Sub
'Other events omitted.
'Other properties and methods omitted.
The codes run well in my ActiveX component. But there should be better way to do it provided more kernel details of relationship between data source and consumers. MS? Adodc Control doesn?t make any change to the DataSource property and make things done. So if anyone knows how MS does it, give me a hint. I?ll appreciate it.
My test codes are very similar to the sample of the AxData come with the VS6.0. I haven?t added any business rules into it yet. So my problem exists in the sample too.
Hi, Bahnass,
If I was as smart as you to use the tag property, I shouldn?t have spent so much time to do the things described bellow. But when I received your hint, I had already started to work on the thoughts and couldn?t stop. Any way, I figured out a way that will not require users to make a tag while they bind a data consumer control to my data source component. It?s very tedious, though. Please keep me posted if you get any new idea. Thanks.
-----
I have a bit study on data consumer controls? DataSource property. It?s usually a string (some like datagrids are not) at design time and object at run time. When it?s an object, it?s still very special that you can know its type is the type of the data source bound to the consumer but you just can?t get access to any members of the kind of object. That is a problem if you have a few instance of that kind of data source because you will not be able to tell which exactly the data consumer is being bound to. Let?s say we have a data source control typed MyAdodc and have inserted to a form three instances of it, named MyAdodc1, MyAdodc2 and MyAdodc3 respectively. We bound them relatively to three groups of data consumers: (Datagrid1, Text1, Text2), (Datagrid2, Text3, Text4) and (Datagrid3, Text5, Text6). Each of these consumers? Data Source property is an object of MyAdodc type, we are not sure which instance it is bound to.
My solution is to try to save binding relation as a property at design time so we can get the information that helps us when rebinding at run time. Since the DataSource property is a string that contains the data source instance?s name, we can save this information to a private property (call CollectDataConsumer method) of MyAdodc when WriteProperties event rises. I tried to save them to a collection property and failed because I got an error message saying, ?object can?t save because it doesn?t support persistence?. So I have to save them as a string and then parse it to a collection when ReadProoerties event rises. To make sure we always get the property saved, we also need to raise a PropertyChanged event when getDataMember method is called. When users refresh the recordset, we need call the RebindDataConsumer method. Below are parts of the codes:
Withevents rs As adodb.Recordset
Private m_Ctls As Collection
Private m_strCtls As String
Private ctl As Control
Private myself As Object
Private isInRequery As Boolean
Private dontRaiseEvent As Boolean
'Events statements omitted.
Private Sub parseCtls()
Dim tmpstr As String
Dim tmpstrCtls As String
Dim tmplen As Integer
If Len(m_strCtls) = 0 Then
Set m_Ctls = Nothing
Exit Sub
Else
Set m_Ctls = New Collection
End If
tmpstrCtls = m_strCtls
tmplen = InStr(1, tmpstrCtls, ";")
Do While tmplen > 1
tmpstr = Mid(tmpstrCtls, 1, tmplen - 1)
m_Ctls.Add tmpstr, tmpstr
tmpstrCtls = Mid(tmpstrCtls, tmplen + 1, Len(tmpstrCtls) - tmplen)
tmplen = InStr(1, tmpstrCtls, ";")
Loop
If Len(tmpstrCtls) > 0 Then
m_Ctls.Add tmpstrCtls, tmpstrCtls
End If
If m_Ctls.Count = 0 Then Set m_Ctls = Nothing
End Sub
Private Sub CollectDataConsumers()
Dim tmpstr As String
Dim tmpdb As DataBinding
'Clear the string because we want to do it all over
m_strCtls = ""
For Each ctl In Extender.Parent.Controls
On Error Resume Next
'Get the type of the datasource property.
'if encountered an error, try next since it must not be a data consumer
tmpstr = TypeName(ctl.DataSource)
If Err.Number <> 0 Then
Err.Clear
Else
On Error GoTo 0
Debug.Print tmpstr & " " & ctl.Name
If tmpstr = "String" Then
'if the type is string, it contains the data source's instance name.
If ctl.DataSource = Extender.Name Then
m_strCtls = m_strCtls & IIf(Len(m_strCtls) = 0, "", ";") & ctl.Name
End If
Else
'Only when the type name is my type, it can be bound to me.
If tmpstr = TypeName(Me) Then
'still we need to check whether it is bound to exact me.
'At design time, the databindings collection includes bound data source information.
'At run time, we will lose the chance.
For Each tmpdb In ctl.DataBindings
Debug.Print tmpdb.PropertyName & ";" & tmpdb.DataSourceName & ";" & tmpdb.DataMember & ";" & tmpdb.DataField & ";IsDataSource:" & tmpdb.IsDataSource & ";IsBindable:" & tmpdb.IsBindable
If tmpdb.PropertyName = "DataSource" And tmpdb.DataSourceName = Extender.Name Then
m_strCtls = m_strCtls & IIf(Len(m_strCtls) = 0, "", ";") & ctl.Name
End If
Next
End If
End If
End If
Next
Debug.Print m_strCtls
End Sub
Private Sub RebindDataConsumers()
Dim tmpstr As String
'Don't want the user of this component to complain he gets too much events.
Dim isFirstBinding As Boolean
'Get the control of myself. It?s different from Me.
If myself Is Nothing Then
For Each ctl In Parent.Controls
If ctl.Name = Extender.Name Then
Set myself = ctl
Exit For
End If
Next
End If
isFirstBinding = True
For Each ctl In Parent.Controls
On Error Resume Next
tmpstr = TypeName(ctl.DataSource)
If Err.Number <> 0 Then
Err.Clear
Else
On Error GoTo 0
Debug.Print tmpstr & " " & ctl.Name
If tmpstr = "String" Then
'I don't think it could be a string.
'This is just for safe
If ctl.DataSource = Extender.Name Then
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
Else
If tmpstr = TypeName(Me) Then
'Initially, it goes here.
'we have to check whether it's in our list.
If isCtlIn(ctl) Then
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
End If
If tmpstr = Extender.Name Then
'Once we have set myself to the datasource property, it goes here.
If Not isFirstBinding Then
dontRaiseEvent = True
End If
Set ctl.DataSource = myself
If Not isFirstBinding Then
dontRaiseEvent = False
End If
isFirstBinding = False
End If
End If
End If
Next
End Sub
Private Function isCtlIn(ByVal vCtl As Control) As Boolean
Dim tmpCtl As Variant
isCtlIn = False
If m_Ctls Is Nothing Then Exit Function
For Each tmpCtl In m_Ctls
If tmpCtl = vCtl.Name Then
isCtlIn = True
Exit For
End If
Next
End Function
Private Sub UserControl_GetDataMember(
'If strCtls Is empty, set the property dirty
'So it will invoke WriteProperties to call CollectDataConsumers if this is at design time.
If Len(strCtls) = 0 Then strCtls = m_strCtls
'Omitted other codes.
End Sub
Private Sub UserControl_ReadProperties
'Omitted other codes.
m_strCtls = PropBag.ReadProperty("strC
parseCtls
End Sub
Private Sub UserControl_WritePropertie
'Omitted other codes.
CollectDataConsumers
strCtls = m_strCtls
Call PropBag.WriteProperty("str
End Sub
Private Sub rs_WillMove(ByVal adReason As EventReasonEnum, adStatus As EventStatusEnum, ByVal pRecordset As Recordset)
If Not dontRaiseEvent Then RaiseEvent WillMove(adReason, adStatus, pRecordset)
End Sub
Private Sub rs_MoveComplete(ByVal adReason As EventReasonEnum, ByVal pError As Error, adStatus As EventStatusEnum, ByVal pRecordset As Recordset)
If Not dontRaiseEvent Then RaiseEvent MoveComplete(adReason, pError, adStatus, pRecordset)
If adStatus = adStatusOK And isInRequery Then
'The undeniable move is done.
'Erase the mark and invoke the rebinding.
isInRequery = False
RebindDataConsumers
End If
If adStatus = adStatusOK And adReason = adRsnRequery Then
'Requery finished. It will take an undeniable move.
'So only make a mark here
isInRequery = True
End If
End Sub
'Other events omitted.
'Other properties and methods omitted.
The codes run well in my ActiveX component. But there should be better way to do it provided more kernel details of relationship between data source and consumers. MS? Adodc Control doesn?t make any change to the DataSource property and make things done. So if anyone knows how MS does it, give me a hint. I?ll appreciate it.
ASKER CERTIFIED SOLUTION
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
ASKER
Hi, WaterDog, thank you very much. I just tested it. It works fine.
I'd definitely give you an A.
I'd definitely give you an A.
Just REBOUND Them
for each ctrl in controls
if ctrl.tag="myAdo" then
set ctrl.datasource = myado
end if
next
' Note DataField Property not affected