Home > Archive > Visual Basic Controls > January 2006 > Weird PropertyBag problem, values won't change
You are viewing an archived Text-only version of the thread.
To view this thread in it's original format and/or if you want to reply to
this thread please [click here]
| Author |
Weird PropertyBag problem, values won't change
|
|
| Jonathan 2006-01-24, 7:07 pm |
| I'm in the process of writing a couple server programs that use
sessions to control what goes on with users. Instead of using classes
or structures to store this information, I'm using PropertyBags for two
reasons: 1) I can store the data in a file if need be, and 2) I can
dynamically add values as needed. Since all the values being stored are
scalars (no objects of any kind, all booleans, strings, integers,
etc.), I figured this would make life much easier. One of the values
used is a Timeout value to kick users off that have been idle for too
long. I have two classes:
CSession - Data storage class for a session. This is basically a
wrapper for the PropertyBag object.
CSessions - Collection of CSession objects for all logged in users.
Here's my code for CSession:
---------------------------------------------------------------
Option Explicit
Private m_Data As New PropertyBag
Private m_ID As String
Public Property Let ID(ByVal vData As String)
m_ID = vData
End Property
Public Property Get ID() As String
ID = m_ID
End Property
Public Property Get Item(ByVal Key As String) As String
On Error Resume Next
Item = m_Data.ReadProperty(Key)
On Error GoTo 0
End Property
Public Property Let Item(ByVal Key As String, ByVal Data As String)
m_Data.WriteProperty Key, Data
End Property
---------------------------------------------------------------
Fairly simple, right?
And the code for CSessions:
---------------------------------------------------------------
Option Explicit
'local variable to hold collection
Private mCol As Collection
Public Function Add(ID As String) As CSession
'create a new object
Dim objNewMember As CSession
Set objNewMember = New CSession
'set the properties passed into the method
objNewMember.ID = ID
mCol.Add objNewMember, ID
'return the object created
Set Add = objNewMember
Set objNewMember = Nothing
End Function
'// Item -- returns a given CSession object. This code was originally
'// generated from the VB Class Builder wizard. However, in an effort
to be
'// more flexible, I have modified it to add a new CSession object if
the
'// requested one doesn't exist. Thus, it is now perfectly legal to
say:
'//
'// CSessions("ThisItem") = "Hello, world!"
'//
'// (if "Hello, World" was a session and not a string) even though
'// "ThisItem" doesn't exist in the collection.
'//
Public Property Get Item(ID As String) As CSession
If Exists(ID) Then
Set Item = mCol(ID)
Else
Set Item = Add(ID)
End If
End Property
Public Property Get Count() As Long
Count = mCol.Count
End Property
Public Sub Remove(ID As String)
mCol.Remove ID
End Sub
'// Exists -- Dirty little hack to see if a specified CSession object
'// exists within the collection.
'//
Public Function Exists(ID As String) As Boolean
On Error Resume Next
Exists = (Not (mCol(ID) Is Nothing))
End Function
Public Property Get NewEnum() As IUnknown
'this property allows you to enumerate
'this collection with the For...Each syntax
Set NewEnum = mCol.[_NewEnum]
End Property
Private Sub Class_Initialize()
'creates the collection when this class is created
Set mCol = New Collection
End Sub
Private Sub Class_Terminate()
'destroys collection when this class is terminated
Set mCol = Nothing
End Sub
---------------------------------------------------------------
When a user logs in, I call the Add method on a CSessions object with a
automatically generated session ID. They are then immediately given a
Timeout of "now + 30 seconds" or whatever the program calls for. In
other words:
Private Sessions As New CSessions
Sub Login(User)
Dim oSess As CSession
Set oSess = Sessions.Add(NewSessionID)
oSess("Timeout") = DateAdd("s", 30, Now)
'Other session data follows
End Sub
Now, that's all well and good. No problem. I can access the Timeout
value later on via Sessions(SessionID)("Timeout") or any other value
that I store on login.
However, when I go to change any of the values, they don't stay
written. Here's what I mean:
Debug.Print "Current timeout: " & Sessions(SessionID)("Timeout")
UpdateTimeout SessionID
Debug.Print "New timeout: " & Sessions(SessionID)("Timeout")
Sub UpdateTimeout(ByVal SessionID As String)
Debug.Print "Updating timeout on session " & SessionID
Sessions(SessionID)("Timeout") = DateAdd("s", 30, Now)
Debug.Print "Timeout has been changed to " &
Sessions(SessionID)("Timeout")
End Sub
Upon being run, the debug window goes something like this:
Current timeout: 1/17/2006 2:18:43 AM
Updating timeout on session HF2
Timeout has been changed to 1/17/2006 2:19:13 AM
New timeout: 1/17/2006 2:18:43 AM
The timeout is ONLY UPDATED inside the sub procedure. As soon as it
exits, it's reverted back to the way it was before.
Does ANYONE have ANY IDEA what is going on with my code?
| |
| Jonathan 2006-01-24, 9:58 pm |
| Okay, here's a COMPLETE program to replicate the problem:
-----------------------------------------------------------------
Option Explicit
Public pBag As New PropertyBag
Public Sub Main()
Dim i As Integer
Dim nTime As Long
pBag.WriteProperty "Timeout", DateAdd("s", 30, Now)
Debug.Print "Timeout = " & pBag.ReadProperty("Timeout")
nTime = Timer
Do While Timer < nTime + 5
DoEvents
Loop
UpdateTimeout
nTime = Timer
Do While Timer < nTime + 5
DoEvents
Loop
Debug.Print "Timeout = " & pBag.ReadProperty("Timeout")
nTime = Timer
Do While Timer < nTime + 5
DoEvents
Loop
pBag.WriteProperty "Timeout", DateAdd("s", 30, Now)
Debug.Print "Timeout = " & pBag.ReadProperty("Timeout")
End Sub
Public Sub UpdateTimeout()
pBag.WriteProperty "Timeout", DateAdd("s", 30, Now)
Debug.Print "Updated timeout = " & pBag.ReadProperty("Timeout")
End Sub
----------------------------------------------------------------
Look VERY CLOSELY at the output in the debug window...
Any ideas?
| |
| Jonathan 2006-01-24, 9:58 pm |
| Okay, I came across something very discouraging:
---------------------------------------------------
Option Explicit
Public pBag As New PropertyBag
Public Sub Main()
pBag.WriteProperty "Username", "John"
Debug.Print pBag.ReadProperty("Username")
pBag.WriteProperty "Username", "Tony"
Debug.Print pBag.ReadProperty("Username")
pBag.WriteProperty "Username", "Mitch"
Debug.Print pBag.ReadProperty("Username")
End Sub
---------------------------------------------------
To which the debug window displays:
---------------------------------------------------
John
John
John
---------------------------------------------------
It seems as if the PropertyBag object holds on to the values once
they're written. They simply can't be changed. I suppose I'm going to
have to implement my own storage class.
| |
| Kevin Provance 2006-01-25, 4:16 am |
| Personally, I would go about this a different way. Decalring a variable as
New can have wacky results. Try something along these lines (this is air
code, I've not tested it.)
===BEGIN CODE===
Option Explicit
Public mPBag As PropertyBag
Public Sub Main()
Set mpBag = New PropertyBag
With mpBag
.WriteProperty "Username", "John"
Debug.Print .ReadProperty("Username", "John")
.WriteProperty "Username", "Tony"
Debug.Print .ReadProperty("Username", "Tony")
.WriteProperty "Username", "Mitch"
Debug.Print .ReadProperty("Username", "Mitch")
End With
Set mPBag = Nothing
End Sub
===END CODE===
Try that.
- Kev
"Jonathan" <jvstech@gmail.com> wrote in message
news:1138158339.715049.53540@f14g2000cwb.googlegroups.com...
> Okay, I came across something very discouraging:
>
> ---------------------------------------------------
> Option Explicit
>
> Public pBag As New PropertyBag
>
> Public Sub Main()
> pBag.WriteProperty "Username", "John"
> Debug.Print pBag.ReadProperty("Username")
> pBag.WriteProperty "Username", "Tony"
> Debug.Print pBag.ReadProperty("Username")
> pBag.WriteProperty "Username", "Mitch"
> Debug.Print pBag.ReadProperty("Username")
> End Sub
> ---------------------------------------------------
>
> To which the debug window displays:
>
> ---------------------------------------------------
> John
> John
> John
> ---------------------------------------------------
>
> It seems as if the PropertyBag object holds on to the values once
> they're written. They simply can't be changed. I suppose I'm going to
> have to implement my own storage class.
>
| |
| Andy DF 2006-01-25, 4:16 am |
| Ok, I know what's happening here:
Everytime WriteProperty is called it appends a NEW property, with the
specified name, EVEN IF IT ALREADY EXISTS!!
And when ReadProperty is called, it just returns the first property with the
specified name that it finds.
Major bug!!!
They probably, oops, forgot to implement a DeleteProperty method.
I guess one way to work around this would be to modify the byte array
returned by the Contents property.
Docs on PropertyBag needed...
Try this code and look closely at the output (I was doing some testing so
it's a bit messy):
' ****************************************
*************
Option Explicit
Public pBag As PropertyBag
Private Const PROP_USERNAME As String = "UserName"
Private Sub Form_Click()
Static iTime As Integer
iTime = iTime + 1
If iTime = 4 Then iTime = 1
Select Case iTime
Case 1
pWriteRead "John"
Case 2
pWriteRead "Tony"
Case 3
pWriteRead "Smith"
End Select
End Sub
Private Sub Form_Load()
Set pBag = New PropertyBag
End Sub
Private Sub pWriteRead(Name As String)
Dim abByte() As Byte
With pBag
If Len(.ReadProperty(PROP_USERNAME)) Then
abByte = .Contents
Debug.Print abByte
End If
.WriteProperty PROP_USERNAME, Name
Debug.Print .ReadProperty(PROP_USERNAME)
End With
End Sub
Private Sub Form_Unload(Cancel As Integer)
Set pBag = Nothing
End Sub
' ****************************************
***************
"Kevin Provance" <casey@tpasoft.com> ha scritto nel messaggio
news:OybF6mWIGHA.2212@TK2MSFTNGP15.phx.gbl...
> Personally, I would go about this a different way. Decalring a variable
> as New can have wacky results. Try something along these lines (this is
> air code, I've not tested it.)
>
> ===BEGIN CODE===
> Option Explicit
>
> Public mPBag As PropertyBag
>
> Public Sub Main()
>
> Set mpBag = New PropertyBag
> With mpBag
> .WriteProperty "Username", "John"
> Debug.Print .ReadProperty("Username", "John")
>
> .WriteProperty "Username", "Tony"
> Debug.Print .ReadProperty("Username", "Tony")
>
> .WriteProperty "Username", "Mitch"
> Debug.Print .ReadProperty("Username", "Mitch")
> End With
> Set mPBag = Nothing
>
> End Sub
>
> ===END CODE===
>
> Try that.
>
> - Kev
>
> "Jonathan" <jvstech@gmail.com> wrote in message
> news:1138158339.715049.53540@f14g2000cwb.googlegroups.com...
>
>
| |
| Bob Butler 2006-01-25, 7:58 am |
| "Andy DF" <nospam@nospam.com> wrote in message
news:43d7426f$0$333$5fc30a8@news.tiscali.it
> Ok, I know what's happening here:
>
> Everytime WriteProperty is called it appends a NEW property, with the
> specified name, EVEN IF IT ALREADY EXISTS!!
> And when ReadProperty is called, it just returns the first property
> with the specified name that it finds.
> Major bug!!!
This is not a bug, it is by design. Try this:
Private Sub Main()
Dim p As PropertyBag
Dim x As Long
Set p = New PropertyBag
For x = 1 To 5
p.WriteProperty "myprop", "value" & CStr(x)
Next
For x = 1 To 5
Debug.Print p.ReadProperty("myprop")
Next
Set p = Nothing
End Sub
It is an easy way to store a list in a propertybag. You can store another
property with the count of items added and then retrieve them all using the
same name. It looks like you are trying to use a property bag like a
collection or dictionary with the property name acting as the key to a
single item and that's just not how the object works. Maybe if you describe
what your real need is somebody can suggest a better way to accomplish it.
--
Reply to the group so all can participate
VB.Net: "Fool me once..."
| |
| Andy DF 2006-01-25, 7:19 pm |
| Bob,
I'm not the original OP.
I see your point but I can't help but it seeing it as a bug.
To me that just looks like poor implementation.
If I write a "Name" property, I want to be able to read it, overwrite it and
delete it.
Or maybe the name PropertyBag isn't appropriate after all, maybe it should
have been named ListBag.
There is no evidence in the MSDN of this so called "feature".
FWIW as soon as I have some spare time I'll write a wrapper class that will
expose a DeleteProperty method, probably by parsing thru the byte array to
find the property to be removed.
Andy
"Bob Butler" <tiredofit@nospam.com> ha scritto nel messaggio
news:%23iJq6RbIGHA.668@TK2MSFTNGP11.phx.gbl...
> "Andy DF" <nospam@nospam.com> wrote in message
> news:43d7426f$0$333$5fc30a8@news.tiscali.it
>
> This is not a bug, it is by design. Try this:
>
> Private Sub Main()
> Dim p As PropertyBag
> Dim x As Long
> Set p = New PropertyBag
> For x = 1 To 5
> p.WriteProperty "myprop", "value" & CStr(x)
> Next
> For x = 1 To 5
> Debug.Print p.ReadProperty("myprop")
> Next
> Set p = Nothing
> End Sub
>
> It is an easy way to store a list in a propertybag. You can store another
> property with the count of items added and then retrieve them all using
> the
> same name. It looks like you are trying to use a property bag like a
> collection or dictionary with the property name acting as the key to a
> single item and that's just not how the object works. Maybe if you
> describe
> what your real need is somebody can suggest a better way to accomplish it.
>
>
> --
> Reply to the group so all can participate
> VB.Net: "Fool me once..."
>
| |
| Bob Butler 2006-01-25, 7:19 pm |
| "Andy DF" <nospam@nospam.com> wrote in message
news:43d78889$0$345$5fc30a8@news.tiscali.it
> Bob,
>
> I'm not the original OP.
> I see your point but I can't help but it seeing it as a bug.
A rose by any other name! <g>
> To me that just looks like poor implementation.
Maybe; it can be useful but it is a "unique" design and I would definitely
agree that it is counter-intuitive when you first run into it.
> If I write a "Name" property, I want to be able to read it, overwrite
> it and delete it.
Then use a collection or a dictionary; a PB is temporary holding area used
to save a set of values until they are needed again. It's not meant to be
dynamic.
> Or maybe the name PropertyBag isn't appropriate after all, maybe it
> should have been named ListBag.
It still is just holding propeties, it's just that you can have more than 1
value under any given property name. I usually avoid doing that but, again,
that's how the PB works.
> There is no evidence in the MSDN of this so called "feature".
It's definitely not well documented. I had once found an article on it but
with MS scrubbing all references to VB classic it's getting harder and
harder to find things like this.
> FWIW as soon as I have some spare time I'll write a wrapper class
> that will expose a DeleteProperty method, probably by parsing thru
> the byte array to find the property to be removed.
In general, a property bag is intended to be created and filled, then passed
to another (or persisted and later depersisted by the same object) where it
is "emptied" and destroyed. It's not meant to be used as a static place to
store values that are changing. When used as intended there's no need for a
DeleteProperty method because you're just going to read them all once and
then discard the object. If you find yourself in need of a DeleteProperty
method then I'd say it's more likely that you need to switch to a Collection
or some other object because the PB is probably a poor choice for the
situation.
--
Reply to the group so all can participate
VB.Net: "Fool me once..."
| |
| vorpus@gmail.com 2006-01-25, 7:19 pm |
| I still find the PropertyBag to be ridiculously useful. I use it in
client/server data transmission because I can very easily parse the
information in a packet when it arrives. However, where I need it MOST
it simply falls short.
Luckily, I wrote a replacement which is basically a wrapper for a
collection. It works only with scalar values, and it works very nicely
if you don't plan on saving all of the data anywhere (like you can with
a PropertyBag.) That's soon to change. I started working on the
Contents property, but it's proving to be a bit more challenging.
Originally, I was just going to dump everything to a PropertyBag and
return its Contents, but reading in from a PropertyBag doesn't afford
me any way to read the Keys of the stored data. If anyone here knows
how to work with variant strings and variant memory in general WITHOUT
using OLE streams, please help me out.
Here's the code I have so far:
ScalarStorage.cls:
'---------------------------------------------------------------------------------
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"
(Destination As Any, Source As Any, ByVal Length As Long)
Private Const BASE_ERR = vbObjectError + 2493
Private mCol As Collection
Private bLocked As Boolean
Private Sub Class_Initialize()
Set mCol = New Collection
End Sub
Private Sub Class_Terminate()
Set mCol = Nothing
End Sub
Private Sub Add(ByVal Key As String, vData)
If Len(Key) = 0 Then
Err.Raise BASE_ERR + 1, "ScalarStorage", "Key cannot be empty."
Exit Sub
End If
Dim oData As ScalarStorageData
Set oData = New ScalarStorageData
oData.Key = Key
oData.Data = vData
mCol.Add oData, Key
Set oData = Nothing
End Sub
Public Function Exists(ByVal ID As String) As Boolean
On Error Resume Next
Exists = (Not mCol(ID) Is Nothing)
On Error GoTo 0
End Function
Public Property Get Item(ByVal Key As String) As Variant
If Exists(Key) Then
Item = mCol(Key).Data
End If
End Property
Public Property Let Item(ByVal Key As String, vData)
If Not bLocked Then
If Exists(Key) Then
mCol(Key) = vData
Else
Add Key, vData
End If
Else
Err.Raise BASE_ERR + 2, "ScalarStorage", "Data is locked while
retrieving contents."
Exit Property
End If
End Property
Public Property Get Contents() As Variant
'// Convert the entire collection to a byte array
'// (THIS FUNCTION DOESN'T WORK WELL AT ALL.)
Dim aContents() As Byte
Dim aKeyName() As Byte
Dim nKeyLen As Integer
Dim nItemLen As Long
Dim nTotalSize As Long
Dim i As Long, nPos As Long
If mCol.Count = 0 Then Exit Property
nTotalSize = 0
For i = 1 To mCol.Count
nTotalSize = nTotalSize + 2
nTotalSize = nTotalSize + Len(mCol(i).Key)
nTotalSize = nTotalSize + 4
nTotalSize = nTotalSize + LenB(mCol(i).Data)
Next i
ReDim aContents(0 To nTotalSize - 1) As Byte
nPos = 0
For i = 1 To mCol.Count
aKeyName = StrConv(mCol(i).Key, vbFromUnicode)
nKeyLen = Len(mCol(i).Key)
nItemLen = LenB(mCol(i).Data)
CopyMemory ByVal VarPtr(aContents(nPos)),
CInt(Len(mCol(i).Key)), 2
nPos = nPos + 2
CopyMemory ByVal VarPtr(aContents(nPos)), ByVal
VarPtr(aKeyName(0)), nKeyLen
nPos = nPos + nKeyLen
CopyMemory ByVal VarPtr(aContents(nPos)),
CLng(LenB(mCol(i).Data)), 4
nPos = nPos + 4
CopyMemory ByVal VarPtr(aContents(nPos)), ByVal
VarPtr(mCol(i).Data), nItemLen
nPos = nPos + nItemLen
Next i
Contents = aContents
End Property
'---------------------------------------------------------------------------------
And for ScalarStorageData.cls:
'---------------------------------------------------------------------------------
Option Explicit
Public Key As String
Public Data As Variant
'---------------------------------------------------------------------------------
| |
| Jonathan 2006-01-25, 7:19 pm |
| I still find the PropertyBag to be ridiculously useful. I use it in
client/server data transmission because I can very easily parse the
information in a packet when it arrives. However, where I need it MOST
it simply falls short.
Luckily, I wrote a replacement which is basically a wrapper for a
collection. It works only with scalar values, and it works very nicely
if you don't plan on saving all of the data anywhere (like you can with
a PropertyBag.) That's soon to change. I started working on the
Contents property, but it's proving to be a bit more challenging.
Originally, I was just going to dump everything to a PropertyBag and
return its Contents, but reading in from a PropertyBag doesn't afford
me any way to read the Keys of the stored data. If anyone here knows
how to work with variant strings and variant memory in general WITHOUT
using OLE streams, please help me out.
Here's the code I have so far:
ScalarStorage.cls:
'---------------------------------------------------------------------------------
Option Explicit
Private Declare Sub CopyMemory Lib "kernel32" Alias "RtlMoveMemory"
(Destination As Any, Source As Any, ByVal Length As Long)
Private Const BASE_ERR = vbObjectError + 2493
Private mCol As Collection
Private bLocked As Boolean
Private Sub Class_Initialize()
Set mCol = New Collection
End Sub
Private Sub Class_Terminate()
Set mCol = Nothing
End Sub
Private Sub Add(ByVal Key As String, vData)
If Len(Key) = 0 Then
Err.Raise BASE_ERR + 1, "ScalarStorage", "Key cannot be empty."
Exit Sub
End If
Dim oData As ScalarStorageData
Set oData = New ScalarStorageData
oData.Key = Key
oData.Data = vData
mCol.Add oData, Key
Set oData = Nothing
End Sub
Public Function Exists(ByVal ID As String) As Boolean
On Error Resume Next
Exists = (Not mCol(ID) Is Nothing)
On Error GoTo 0
End Function
Public Property Get Item(ByVal Key As String) As Variant
If Exists(Key) Then
Item = mCol(Key).Data
End If
End Property
Public Property Let Item(ByVal Key As String, vData)
If Not bLocked Then
If Exists(Key) Then
mCol(Key) = vData
Else
Add Key, vData
End If
Else
Err.Raise BASE_ERR + 2, "ScalarStorage", "Data is locked while
retrieving contents."
Exit Property
End If
End Property
Public Property Get Contents() As Variant
'// Convert the entire collection to a byte array
'// (THIS FUNCTION DOESN'T WORK WELL AT ALL.)
Dim aContents() As Byte
Dim aKeyName() As Byte
Dim nKeyLen As Integer
Dim nItemLen As Long
Dim nTotalSize As Long
Dim i As Long, nPos As Long
If mCol.Count = 0 Then Exit Property
nTotalSize = 0
For i = 1 To mCol.Count
nTotalSize = nTotalSize + 2
nTotalSize = nTotalSize + Len(mCol(i).Key)
nTotalSize = nTotalSize + 4
nTotalSize = nTotalSize + LenB(mCol(i).Data)
Next i
ReDim aContents(0 To nTotalSize - 1) As Byte
nPos = 0
For i = 1 To mCol.Count
aKeyName = StrConv(mCol(i).Key, vbFromUnicode)
nKeyLen = Len(mCol(i).Key)
nItemLen = LenB(mCol(i).Data)
CopyMemory ByVal VarPtr(aContents(nPos)),
CInt(Len(mCol(i).Key)), 2
nPos = nPos + 2
CopyMemory ByVal VarPtr(aContents(nPos)), ByVal
VarPtr(aKeyName(0)), nKeyLen
nPos = nPos + nKeyLen
CopyMemory ByVal VarPtr(aContents(nPos)),
CLng(LenB(mCol(i).Data)), 4
nPos = nPos + 4
CopyMemory ByVal VarPtr(aContents(nPos)), ByVal
VarPtr(mCol(i).Data), nItemLen
nPos = nPos + nItemLen
Next i
Contents = aContents
End Property
'---------------------------------------------------------------------------------
And for ScalarStorageData.cls:
'---------------------------------------------------------------------------------
Option Explicit
Public Key As String
Public Data As Variant
'---------------------------------------------------------------------------------
It's all the Contents property from there on out. And perhaps a bit of
error checking, like to make sure a UDT or an Object weren't passed in.
|
|
|
|
|