reference subform of subform from 4th form - forms
I have tried every suggestion from many different sites, but none of them work, not even Microsoft's KB articles or references suggested in Stack Overflow.
I have a main form [frmMain], with a sub form called [frmTaskTracking] and a sub form within that sub form called [sfmActivites subform]. I need to obtain the filter for [sfmActivites subform] from a popup form [frmExportTasks] which is opened from [frmTaskTracking] as such:
[frmMain]
[frmTaskTracking]
[sfmActivites subform]
Filter
[frmExportTasks]
What is the correct way to reference the filter for the form [sfmActivites subform] in VBA??
Thanks so much!
Your question is very conceptual, so this answer may or may not apply to your specific issue.
I once had to create a CRUD app involving master-detail data, and I had to do it in Excel VBA, and didn't have access to the database... so I wrote the code against abstractions and implemented Model-View-Presenter, Command and Repository+UnitOfWork patterns... which might be slightly overkill for your needs.
However overkill this solution might be, it's as SOLID as VBA gets, and has allowed me to reuse the same form/view for every single one of the "master" and "details" tables I wanted to work with - again, your post isn't exactly crystal-clear on what it is exactly that you're doing, so I'm just going to expose the solution that worked for me. Is it the right way? Depends what you're doing. It was the right way for me, as I could test the entire functionality with mock data, and everything just worked when I got to the office and swapped the unit of work for one that actually connected to the database.
The key point is that the Presenter knows its MasterId, and its DetailsPresenter if it has one:
IPresenter
Option Explicit
Public Property Get UnitOfWork() As IUnitOfWork
End Property
Public Property Set UnitOfWork(ByVal value As IUnitOfWork)
End Property
Public Property Get View() As IView
End Property
Public Property Set View(ByVal value As IView)
End Property
Public Sub Show()
End Sub
Public Function ExecuteCommand(ByVal commandId As CommandType) As Variant
End Function
Public Function CanExecuteCommand(ByVal commandId As CommandType) As Boolean
End Function
Public Property Get DetailsPresenter() As IPresenter
End Property
Public Property Set DetailsPresenter(ByVal value As IPresenter)
End Property
Public Property Get MasterId() As Long
End Property
Public Property Let MasterId(ByVal value As Long)
End Property
Say I have a CategoriesPresenter and a SubCategoriesPresenter, I could have the CategoriesPresenter implemented like this:
Option Explicit
Private Type tPresenter
UnitOfWork As IUnitOfWork
DetailsPresenter As IPresenter
View As IView
End Type
Private this As tPresenter
Implements IPresenter
Implements IDisposable
Public Property Get UnitOfWork() As IUnitOfWork
Set UnitOfWork = this.UnitOfWork
End Property
Public Property Set UnitOfWork(ByVal value As IUnitOfWork)
Set this.UnitOfWork = value
End Property
Public Property Get View() As IView
Set View = this.View
End Property
Public Property Set View(ByVal value As IView)
Set this.View = value
End Property
Public Property Get DetailsPresenter() As IPresenter
Set DetailsPresenter = this.DetailsPresenter
End Property
Public Property Set DetailsPresenter(ByVal value As IPresenter)
Set this.DetailsPresenter = value
End Property
Public Sub Show()
IPresenter_ExecuteCommand RefreshCommand
View.Show
End Sub
Private Function NewCategory(Optional ByVal id As Long = 0, Optional ByVal description As String = vbNullString) As SqlResultRow
Dim result As SqlResultRow
Dim values As New Dictionary
values.Add "id", id
values.Add "description", description
Set result = UnitOfWork.Repository("Categories").NewItem(View.Model, values)
Set NewCategory = result
End Function
Private Sub Class_Terminate()
Dispose
End Sub
Private Sub Dispose()
If Not View Is Nothing Then Unload View
Disposable.Dispose this.UnitOfWork
Disposable.Dispose this.DetailsPresenter
Set this.UnitOfWork = Nothing
Set this.View = Nothing
Set this.DetailsPresenter = Nothing
End Sub
Private Sub IDisposable_Dispose()
Dispose
End Sub
Private Function IPresenter_CanExecuteCommand(ByVal commandId As CommandType) As Boolean
Dim result As Boolean
Select Case commandId
Case CommandType.CloseCommand, CommandType.RefreshCommand, CommandType.AddCommand
result = True
Case CommandType.DeleteCommand, _
CommandType.EditCommand
result = (Not View.SelectedItem Is Nothing)
Case CommandType.ShowDetailsCommand
If View.SelectedItem Is Nothing Then Exit Function
result = GetDetailsModel.Count > 0
End Select
IPresenter_CanExecuteCommand = result
End Function
Private Property Set IPresenter_DetailsPresenter(ByVal value As IPresenter)
Set DetailsPresenter = value
End Property
Private Property Get IPresenter_DetailsPresenter() As IPresenter
Set IPresenter_DetailsPresenter = DetailsPresenter
End Property
Private Function GetDetailsModel() As SqlResult
Set GetDetailsModel = DetailsPresenter.UnitOfWork.Repository("SubCategories") _
.GetAll _
.WhereFieldEquals("CategoryId", View.SelectedItem("Id"))
End Function
Private Function IPresenter_ExecuteCommand(ByVal commandId As CommandType) As Variant
Select Case commandId
Case CommandType.CloseCommand
View.Hide
Case CommandType.RefreshCommand
Set View.Model = UnitOfWork.Repository("Categories").GetAll
Case CommandType.ShowDetailsCommand
Set DetailsPresenter.View.Model = GetDetailsModel
DetailsPresenter.MasterId = View.SelectedItem("id")
DetailsPresenter.Show
Case CommandType.AddCommand
ExecuteAddCommand
Case CommandType.DeleteCommand
ExecuteDeleteCommand
Case CommandType.EditCommand
ExecuteEditCommand
End Select
End Function
Private Sub ExecuteAddCommand()
Dim description As String
If Not RequestUserInput(prompt:=GetResourceString("AddCategoryMessageText"), _
title:=GetResourceString("AddPromptTitle"), _
outResult:=description, _
default:=GetResourceString("DefaultCategoryDescription")) _
Then
Exit Sub
End If
UnitOfWork.Repository("Categories").Add NewCategory(description:=description)
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End Sub
Private Sub ExecuteDeleteCommand()
Dim id As Long
id = View.SelectedItem("id")
Dim childRecords As Long
childRecords = GetDetailsModel.Count
If childRecords > 0 Then
MsgBox StringFormat(GetResourceString("CannotDeleteItemWithChildItemsMessageText"), childRecords), _
vbExclamation, _
GetResourceString("CannotDeleteItemWithChildItemsMessageTitle")
Exit Sub
End If
If RequestUserConfirmation(StringFormat(GetResourceString("ConfirmDeleteItemMessageText"), id)) Then
UnitOfWork.Repository("Categories").Remove id
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End If
End Sub
Private Sub ExecuteEditCommand()
Dim id As Long
id = View.SelectedItem("id")
Dim description As String
If Not RequestUserInput(prompt:=StringFormat(GetResourceString("EditCategoryDescriptionText"), id), _
title:=GetResourceString("EditPromptTitle"), _
outResult:=description, _
default:=View.SelectedItem("description")) _
Then
Exit Sub
End If
UnitOfWork.Repository("Categories").Update id, NewCategory(id, description)
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End Sub
Private Property Let IPresenter_MasterId(ByVal value As Long)
'not implemented
End Property
Private Property Get IPresenter_MasterId() As Long
'not implemented
End Property
Private Property Set IPresenter_UnitOfWork(ByVal value As IUnitOfWork)
Set UnitOfWork = value
End Property
Private Property Get IPresenter_UnitOfWork() As IUnitOfWork
Set IPresenter_UnitOfWork = UnitOfWork
End Property
Private Sub IPresenter_Show()
Show
End Sub
Private Property Set IPresenter_View(ByVal value As IView)
Set View = value
End Property
Private Property Get IPresenter_View() As IView
Set IPresenter_View = View
End Property
The SubCategoriesPresenter looks like this:
Option Explicit
Private Type tPresenter
MasterId As Long
UnitOfWork As IUnitOfWork
DetailsPresenter As IPresenter
View As IView
End Type
Private this As tPresenter
Implements IPresenter
Implements IDisposable
Private Function NewSubCategory(Optional ByVal id As Long = 0, Optional ByVal categoryId As Long = 0, Optional ByVal description As String = vbNullString) As SqlResultRow
Dim result As SqlResultRow
Dim values As New Dictionary
values.Add "id", id
values.Add "categoryid", categoryId
values.Add "description", description
Set result = UnitOfWork.Repository("SubCategories").NewItem(View.Model, values)
Set NewSubCategory = result
End Function
Public Property Get UnitOfWork() As IUnitOfWork
Set UnitOfWork = this.UnitOfWork
End Property
Public Property Set UnitOfWork(ByVal value As IUnitOfWork)
Set this.UnitOfWork = value
End Property
Public Property Get View() As IView
Set View = this.View
End Property
Public Property Set View(ByVal value As IView)
Set this.View = value
View.Resize width:=400
End Property
Public Sub Show()
View.Show
End Sub
Private Sub Class_Terminate()
Dispose
End Sub
Private Sub Dispose()
If Not View Is Nothing Then Unload View
Disposable.Dispose this.UnitOfWork
Disposable.Dispose this.DetailsPresenter
Set this.UnitOfWork = Nothing
Set this.View = Nothing
Set this.DetailsPresenter = Nothing
End Sub
Private Sub IDisposable_Dispose()
Dispose
End Sub
Private Function IPresenter_CanExecuteCommand(ByVal commandId As CommandType) As Boolean
Dim result As Boolean
Select Case commandId
Case CommandType.CloseCommand, _
CommandType.RefreshCommand, _
CommandType.AddCommand
result = True
Case CommandType.DeleteCommand, _
CommandType.EditCommand
result = (Not View.SelectedItem Is Nothing)
End Select
IPresenter_CanExecuteCommand = result
End Function
Private Property Set IPresenter_DetailsPresenter(ByVal value As IPresenter)
'not implemented
End Property
Private Property Get IPresenter_DetailsPresenter() As IPresenter
'not implemented
End Property
Private Sub ExecuteAddCommand()
Dim description As String
If Not RequestUserInput(prompt:=GetResourceString("AddSubCategoryMessageText"), _
title:=GetResourceString("AddPromptTitle"), _
outResult:=description, _
default:=GetResourceString("DefaultSubCategoryDescription")) _
Then
Exit Sub
End If
UnitOfWork.Repository("SubCategories").Add NewSubCategory(categoryId:=this.MasterId, description:=description)
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End Sub
Private Sub ExecuteDeleteCommand()
Dim id As Long
id = View.SelectedItem("id")
If RequestUserConfirmation(StringFormat(GetResourceString("ConfirmDeleteItemMessageText"), id)) Then
UnitOfWork.Repository("SubCategories").Remove id
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End If
End Sub
Private Sub ExecuteEditCommand()
Dim id As Long
id = View.SelectedItem("id")
Dim description As String
If Not RequestUserInput(prompt:=StringFormat(GetResourceString("EditSubCategoryDescriptionText"), id), _
title:=GetResourceString("EditPromptTitle"), _
outResult:=description, _
default:=View.SelectedItem("description")) _
Then
Exit Sub
End If
UnitOfWork.Repository("SubCategories").Update id, NewSubCategory(id, this.MasterId, description)
UnitOfWork.Commit
IPresenter_ExecuteCommand RefreshCommand
End Sub
Private Function IPresenter_ExecuteCommand(ByVal commandId As CommandType) As Variant
Select Case commandId
Case CommandType.CloseCommand
View.Hide
Case CommandType.RefreshCommand
Set View.Model = UnitOfWork.Repository("SubCategories") _
.GetAll _
.WhereFieldEquals("CategoryId", this.MasterId)
Case CommandType.EditCommand
ExecuteEditCommand
Case CommandType.DeleteCommand
ExecuteDeleteCommand
Case CommandType.AddCommand
ExecuteAddCommand
End Select
End Function
Private Property Let IPresenter_MasterId(ByVal value As Long)
this.MasterId = value
End Property
Private Property Get IPresenter_MasterId() As Long
IPresenter_MasterId = this.MasterId
End Property
Private Property Set IPresenter_UnitOfWork(ByVal value As IUnitOfWork)
Set UnitOfWork = value
End Property
Private Property Get IPresenter_UnitOfWork() As IUnitOfWork
Set IPresenter_UnitOfWork = UnitOfWork
End Property
Private Sub IPresenter_Show()
Show
End Sub
Private Property Set IPresenter_View(ByVal value As IView)
Set View = value
End Property
Private Property Get IPresenter_View() As IView
Set IPresenter_View = View
End Property
In your case you would have a DetailsPresenter isntance right here, and that child would also have its own DetailsPresenter instance.
The hardest thing for me, was to implement the commands. Here's something that might help:
CommandCallback
Option Explicit
Private owner As IPresenter
Implements ICommandCallback
Public Property Get CallbackOwner() As IPresenter
Set CallbackOwner = owner
End Property
Public Property Set CallbackOwner(ByVal value As IPresenter)
Set owner = value
End Property
Private Property Set ICommandCallback_CallbackOwner(ByVal value As IPresenter)
Set owner = value
End Property
Private Property Get ICommandCallback_CallbackOwner() As IPresenter
Set ICommandCallback_CallbackOwner = owner
End Property
Private Function ICommandCallback_CanExecute(ByVal cmd As CommandType) As Boolean
If owner Is Nothing Then Exit Function
ICommandCallback_CanExecute = CallByName(owner, "CanExecuteCommand", VbMethod, cmd)
End Function
Private Sub ICommandCallback_Execute(ByVal cmd As CommandType)
If owner Is Nothing Then Exit Sub
If Not ICommandCallback_CanExecute(cmd) Then Exit Sub
CallByName owner, "ExecuteCommand", VbMethod, cmd
End Sub
This allowed me to get the logic completely outside of the view, and into the presenters.
Here's the code-behind for my form:
Option Explicit
Private Type tView
Model As SqlResult
Selection As SqlResultRow
Callback As ICommandCallback
End Type
Private this As tView
'MinSize is determined by design-time size.
Private minHeight As Integer
Private minWidth As Integer
Private layoutBindings As New List
Implements IView
Private Sub IView_Resize(Optional ByVal width As Integer, Optional ByVal height As Integer)
If width <> 0 Then Me.width = width
If height <> 0 Then Me.height = height
End Sub
Private Sub UserForm_Initialize()
BindControlLayouts
minHeight = Me.height
minWidth = Me.width
End Sub
Private Sub BindControlLayouts()
'todo: refactor this
Dim buttonLeftAnchor As Integer
buttonLeftAnchor = EditButton.Left
Dim buttonMargin As Integer
buttonMargin = 2
EditKeyButton.Top = AddButton.Top
EditDateButton.Top = EditKeyButton.Top + EditKeyButton.height + buttonMargin
EditDescriptionButton.Top = EditDateButton.Top + EditDateButton.height + buttonMargin
EditKeyButton.Left = buttonLeftAnchor
EditDateButton.Left = buttonLeftAnchor
EditDescriptionButton.Left = buttonLeftAnchor
Dim instructionsLabelLayout As New ControlLayout
instructionsLabelLayout.Bind Me, InstructionsLabel, AnchorAll
Dim backgroundImageLayout As New ControlLayout
backgroundImageLayout.Bind Me, BackgroundImage, AnchorAll
Dim itemsListLayout As New ControlLayout
itemsListLayout.Bind Me, ItemsList, AnchorAll
Dim closeButtonLayout As New ControlLayout
closeButtonLayout.Bind Me, CloseButton, BottomAnchor + RightAnchor
Dim addButtonLayout As New ControlLayout
addButtonLayout.Bind Me, AddButton, RightAnchor + TopAnchor
Dim editButtonLayout As New ControlLayout
editButtonLayout.Bind Me, EditButton, RightAnchor
Dim showDetailsButtonLayout As New ControlLayout
showDetailsButtonLayout.Bind Me, ShowDetailsButton, RightAnchor
Dim deleteButtonLayout As New ControlLayout
deleteButtonLayout.Bind Me, DeleteButton, RightAnchor
Dim editKeyButtonLayout As New ControlLayout
editKeyButtonLayout.Bind Me, EditKeyButton, RightAnchor
Dim EditDateButtonLayout As New ControlLayout
EditDateButtonLayout.Bind Me, EditDateButton, RightAnchor
Dim EditDescriptionButtonLayout As New ControlLayout
EditDescriptionButtonLayout.Bind Me, EditDescriptionButton, RightAnchor
layoutBindings.Add closeButtonLayout, _
backgroundImageLayout, _
instructionsLabelLayout, _
itemsListLayout, _
addButtonLayout, _
editButtonLayout, _
showDetailsButtonLayout, _
deleteButtonLayout, _
editKeyButtonLayout, _
EditDateButtonLayout, _
EditDescriptionButtonLayout
End Sub
Private Sub UserForm_QueryClose(Cancel As Integer, CloseMode As Integer)
Cancel = True
Hide
End Sub
Private Sub UserForm_Resize()
Application.ScreenUpdating = False
If Me.width < minWidth Then Me.width = minWidth
If Me.height < minHeight Then Me.height = minHeight
Dim layout As ControlLayout
For Each layout In layoutBindings
layout.Resize Me
Next
Application.ScreenUpdating = True
End Sub
Public Property Get Model() As SqlResult
Set Model = this.Model
End Property
Public Property Set Model(ByVal value As SqlResult)
Set this.Model = value
OnModelChanged
End Property
Public Property Get SelectedItem() As SqlResultRow
Set SelectedItem = this.Selection
End Property
Public Property Set SelectedItem(ByVal value As SqlResultRow)
If (Not (value Is Nothing)) Then
If (ObjPtr(value.ParentResult) <> ObjPtr(this.Model)) Then
Set value.ParentResult = this.Model
End If
End If
Set this.Selection = value
EvaluateCanExecuteCommands
End Property
Private Sub EvaluateCanExecuteCommands()
AddButton.Enabled = this.Callback.CanExecute(AddCommand)
CloseButton.Enabled = this.Callback.CanExecute(CloseCommand)
DeleteButton.Enabled = this.Callback.CanExecute(DeleteCommand)
EditButton.Enabled = this.Callback.CanExecute(EditCommand)
ShowDetailsButton.Enabled = this.Callback.CanExecute(ShowDetailsCommand)
EditDateButton.Enabled = EditButton.Enabled
EditDescriptionButton.Enabled = EditButton.Enabled
EditKeyButton.Enabled = EditButton.Enabled
End Sub
Public Sub Initialize(cb As ICommandCallback, ByVal title As String, ByVal instructions As String, ByVal commands As ViewAction)
Localize title, instructions
Set this.Callback = cb
AddButton.Visible = commands And ViewAction.Create
EditButton.Visible = commands And ViewAction.Edit
DeleteButton.Visible = commands And ViewAction.Delete
ShowDetailsButton.Visible = commands And ViewAction.ShowDetails
EditKeyButton.Visible = commands And ViewAction.EditKey
EditDateButton.Visible = commands And ViewAction.EditDate
EditDescriptionButton.Visible = commands And ViewAction.EditDescription
If (commands And PowerEdit) = PowerEdit Then
EditButton.Top = AddButton.Top
Else
EditButton.Top = AddButton.Top + AddButton.height + 2
End If
End Sub
Private Sub Localize(ByVal title As String, ByVal instructions As String)
Me.Caption = title
InstructionsLabel.Caption = instructions
CloseButton.Caption = GetResourceString("CloseButtonText")
AddButton.ControlTipText = GetResourceString("AddButtonToolTip")
EditButton.ControlTipText = GetResourceString("EditButtonToolTip")
DeleteButton.ControlTipText = GetResourceString("DeleteButtonToolTip")
ShowDetailsButton.ControlTipText = GetResourceString("ShowDetailsButtonToolTip")
End Sub
Private Sub OnModelChanged()
ItemsList.Clear
If this.Model Is Nothing Then Exit Sub
this.Model.ValueSeparator = StringFormat("\t")
Dim row As SqlResultRow
For Each row In this.Model
Set row.ParentResult = this.Model
ItemsList.AddItem row.ToString
Next
End Sub
Private Sub ExecuteCommandInternal(method As CommandType)
If this.Callback Is Nothing Then Exit Sub
If this.Callback.CallbackOwner Is Nothing Then Exit Sub
this.Callback.Execute method
End Sub
Private Sub AddButton_Click()
ExecuteCommandInternal AddCommand
End Sub
Private Sub DeleteButton_Click()
ExecuteCommandInternal DeleteCommand
End Sub
Private Sub CloseButton_Click()
ExecuteCommandInternal CloseCommand
End Sub
Private Sub EditButton_Click()
ExecuteCommandInternal EditCommand
End Sub
Private Sub EditKeyButton_Click()
ExecuteCommandInternal EditKeyCommand
End Sub
Private Sub ShowDetailsButton_Click()
ExecuteCommandInternal ShowDetailsCommand
End Sub
Private Sub ItemsList_DblClick(ByVal Cancel As MSForms.ReturnBoolean)
ExecuteCommandInternal EditCommand
End Sub
Private Sub ItemsList_Change()
If ItemsList.ListIndex >= 0 Then
Set SelectedItem = this.Model(ItemsList.ListIndex)
Else
Set SelectedItem = Nothing
End If
End Sub
Private Sub IView_Initialize(cb As ICommandCallback, ByVal title As String, ByVal instructions As String, ByVal commands As ViewAction)
Initialize cb, title, instructions, commands
End Sub
Private Property Get IView_CommandCallback() As ICommandCallback
Set IView_CommandCallback = this.Callback
End Property
Private Property Set IView_Model(ByVal value As SqlResult)
Set Model = value
End Property
Private Property Get IView_Model() As SqlResult
Set IView_Model = Model
End Property
Private Property Set IView_SelectedItem(ByVal value As SqlResultRow)
Set SelectedItem = value
End Property
Private Property Get IView_SelectedItem() As SqlResultRow
Set IView_SelectedItem = SelectedItem
End Property
Private Sub IView_Show()
Show
End Sub
Private Sub IView_Hide()
Hide
End Sub
Obviously you won't be able to use this code as-is without me writing an entire series of blog posts on the subject. But I hope it's enough to illustrate the approach.
Alternatively, you could go the easy way and have a Globals.bas module to share values between forms - there's a balance to achieve between doing it right and getting it done.
Related
Returning editable ADO Recordsets to an MS Access Form using a Class Module
PREFACE: I am using SQL Server 2008 R2 BackEnd and MS Access 2007 for the FrontEnd I have a Class Module that returns any ADO Recordset I want from the SQL Server. I can then assign this to any form RecordSource property. The problem is that when I try to edit the fields it says "This form is read-only" in the status bar. I want the form to be editable. I have two forms FormEntities FormEntitiesEdit The FormEntitiesEdit form does NOT use the Class Module. Rather all the code is in the form itself. The purpose of the class module is avoid redundancy and just be able to use the Class Module to get a recordset from SQL Server easily. FIRST HERE IS MY GLOBAL MODULE 'Default error message. 'eh' stands for error handler Public eh As String 'Global variables for universal use Public conn As ADODB.Connection Public rs As ADODB.Recordset Public com As ADODB.Command SECOND IS THE CLASS MODULE (Name is cADO). THIS CLASS MODULE USES THE conn CONNECTION OBJECT ABOVE Option Explicit Private Const CONST_LockType = 3 Private Const CONST_CursorType = 1 Private Const CONST_CursorLocationServer = 3 Private Const CONST_CursorLocationClient = 2 Private m_Recordset As ADODB.Recordset 'For Public Recordset function Private cSQL$ '********************************************************************** Public Function cGetRecordset(ByRef sql) As ADODB.Recordset Set m_Recordset = New ADODB.Recordset cSQL = sql cOpenRecordset Set cGetRecordset = m_Recordset End Function '********************************************************************** Public Property Set Recordset(Value As ADODB.Recordset) 'Assigns private variable a property If Not Value Is Nothing Then Set m_Recordset = Value End Property '********************************************************************** Public Property Get Recordset() As ADODB.Recordset 'Reads the recordset from the private variable and assigns to new object variable Set Recordset = m_Recordset End Property '********************************** PRIVATE SECTION ********************************** Private Sub cOpenRecordset() On Error GoTo eh 'Ensures that if a recordset is opened from previously that it closes before opening a new one If m_Recordset.State adStateClosed Then m_Recordset.Close Set m_Recordset.ActiveConnection = conn With m_Recordset .LockType = CONST_LockType .CursorType = CONST_CursorType .CursorLocation = CONST_CursorLocationClient .Source = cSQL .Open .Source End With If Not m_Recordset.EOF Then m_Recordset.MoveFirst Exit Sub eh: eh = "Error # " & Str(Err.Number) & " was generated by " & _ Err.Source & Chr(13) & Err.Description MsgBox eh, vbCritical, "Open Recordset System" End Sub '********************************************************************** Private Sub cCloseRecordset() m_Recordset.Close Set m_Recordset = Nothing End Sub '********************************************************************** Private Sub Class_Terminate() If Not (m_Recordset Is Nothing) Then Set m_Recordset = Nothing End Sub THIRD IS THE CODE BEHIND MY FormEntities FORM (USES THE THE cADO CLASS MODULE) Option Explicit Dim db As cADO '********************************************************************** Private Sub Form_Current() LoadTab End Sub '********************************************************************** Private Sub Form_Load() Set db = New cADO FetchRecordSource End Sub '********************************************************************** Private Sub FetchRecordSource() db.cGetRecordset ("SELECT * FROM dbo.Entities") Set Forms("fEntities").Recordset = db.Recordset End Sub FOURTH AND FINALLY IS THE CODE BEHIND THE FormEntitiesEdit FORM (THIS FORM DOES NOT USE THE CLASS MODULE AND I CAN EDIT IT) Option Compare Database Option Explicit Dim rsEntity As New ADODB.Recordset '********************************************************************** Private Sub Form_Load() FetchRecordSource End Sub '********************************************************************** Private Sub FetchRecordSource() Set rsEntity.ActiveConnection = conn 'Sets the record source for the main form With rsEntity .LockType = adLockOptimistic .CursorType = adOpenKeyset .CursorLocation = adUseClient .Source = "SELECT * FROM dbo.Entities" .Open .Source End With Set Forms("fEntitiesEdit").Recordset = rsEntity End Sub '********************************************************************** Private Sub CloseConn() rsEntity.Close End Sub If Access Jet SQL could do the SQL I would bind this form to a Linked Table instead. However I am using a hierarchical (recursive) query which Jet SQL cannot do so I have to bypass the idea of bound forms to Linked Tables. I have .Allow Edits and .AllowAdditions on the form set to true. I also tried changing the .LockType on the ADO Recordset to adOpenKeyset and adOpenDynamic My LockType is adLockOptimistic As you can see the properties are set correctly to be able to edit the recordset I return. I know this is true because when I use the FormEntitiesEdit form with the same properties it lets me edit the field. But when I use the Class Module to return (using the same properties) it says it's read-only. This is important because as you can see it is a lot simpler to use the Class Module, just need it to return an editable recordset. Anyone have ideas? suggestions?
The problem is here in the class' cOpenRecordset() method: .CursorLocation = CONST_CursorLocationClient Here is where you assign constant values ... Private Const CONST_CursorLocationServer = 3 Private Const CONST_CursorLocationClient = 2 Unfortunately, you swapped those two values. Here are the ADODB.CursorLocationEnum constants ... adUseClient = 3 adUseServer = 2 So your class is effectively doing this ... .CursorLocation = adUseServer But you need a client-side cursor if you want the recordset to be editable. I think your class approach may work if you simply redefine the constant, or you will expose a different problem ... Private Const CONST_CursorLocationClient = 3 FormEntitiesEdit is editable because you're using the correct constant there ... .CursorLocation = adUseClient
how to set up readonly property in excel vba class module
I have the following in a class module called clsAgent: Option Explicit Const AgentEmailCol = 9 Private pAgentSheetName As String Private pIDRange As Range Private pAgentID As String Private pAgentEmail As String Public Property Get AgentSheetName() As String AgentSheetName = pAgentSheetName End Property Public Property Let AgentSheetName(AgentSheetName As String) pAgentSheetName = AgentSheetName End Property Public Property Get IDRange() As Range Set IDRange = pIDRange End Property Public Property Set IDRange(IDRange As Range) Set pIDRange = IDRange End Property Public Property Get AgentID() As String AgentID = pAgentID End Property Public Property Let AgentID(AgentID As String) pAgentID = AgentID End Property Public Property Get AgentEmail() As String AgentEmail = WorksheetFunction.VLookup(Me.AgentID, Me.IDRange, AgentEmailCol, False) End Property in a class called clsWorkingRange I have: Option Explicit Private pSheetName As String Private pColNum As Integer Private pTargetRange As Range Public Property Get SheetName() As String SheetName = pSheetName End Property Public Property Let SheetName(SheetName As String) pSheetName = SheetName End Property Public Property Get ColNum() As Integer ColNum = pColNum End Property Public Property Let ColNum(ColNum As Integer) pColNum = ColNum End Property Public Property Get TargetRange() As Range Set TargetRange = pTargetRange End Property Public Property Set TargetRange(TargetRange As Range) Set pTargetRange = TargetRange End Property Private Function Get_Rows_Generic(work_sheet_get As String, column_num As Integer) 'worksheet is the name of a sheet in the form of a string Dim ws As Worksheet: Set ws = Worksheets(work_sheet_get) Dim rows_count As Long: rows_count = ws.Cells(rows.Count, column_num).End(xlUp).Row Get_Rows_Generic = rows_count End Function Public Function set_range(sheet_name As String, col1 As Integer) As Range ' returns the range of a given column using all the rows in a street Dim rows1 As Long rows1 = Get_Rows_Generic(sheet_name, 1) Dim range1 As Range ' range of first search With Worksheets(sheet_name) Set range1 = .Range(.Cells(1, col1), .Cells(rows1, col1)) ' set the range being searched first End With Set set_range = range1 End Function I have a class called clsWorkingSheet: Option Explicit Private pSheetName As String Private pSheet As Worksheet Public Property Get SheetName() As String SheetName = pSheetName End Property Public Property Let SheetName(SheetName As String) pSheetName = SheetName End Property Public Property Get Sheet() As Worksheet Set Sheet = pSheet End Property Public Property Set Sheet(Sheet As Worksheet) Set pSheet = Sheet End Property In a separate module I have the following: Sub test_agent_class() Dim agent_Sheet As clsWorkingSheet Set agent_Sheet = New clsWorkingSheet agent_Sheet.SheetName = "agentsFullOutput.csv" Set agent_Sheet.Sheet = Worksheets(agent_Sheet.SheetName) Dim agent_Range As clsWorkingRange Set agent_Range = New clsWorkingRange Set agent_Range.TargetRange = agent_Range.set_range(agent_Sheet.SheetName, 1) Dim agent_Individual As clsAgent Set agent_Individual = New clsAgent Set agent_Individual.IDRange = agent_Range.TargetRange Debug.Print agent_Individual.IDRange.Address agent_Individual.AgentID = "ObjectID(52d56512763f4a1c608b4753)" Debug.Print agent_Individual.AgentID Debug.Print agent_Individual.AgentEmail end sub I get the following error: Run-time error '1004': Method 'AgentEmail' of object 'clsAgent' failed ΒΈ
Everything here looks fine to me. I've tested it myself and it worked as I believe you expect it should. The only thing I see going wrong is if your agent_Range.TargetRange has less than 9 columns, since that's what you are looking up in your vlookup with Const AgentEmailCol = 9 Based on your updated question/code: It looks like this bit in your clsWorkingRange.set_range() function only returns a range with a single column. You can't do a vlookup that fetches from the 9th column in a 1 column sized range. You'll need to make sure that agent_Range.TargetRange is set to something that is at least 9 columns wide. Check the output of your Debug.Print agent_Individual.IDRange.Address to confirm.
combining classes in vba?
I have a problem finding the right structure of my programme and I am trying to solve it using classes, without success. I have 1 class for Patients, each Patient has the following: Name Operation Surgeon OperationDuration Then each Surgeon should have a schedule for everyday. Therefore, I am thinking using another class for daily schedule, which should have : Day TotalDuration Something in my thinking doesn't look correct and I am struggling to understand what should I do. 1)Is my structure correct for what I want to do? 2)How can I check if a surgeon has a planned daily schedule and if he doesn't then add a patient to his schedule? Any help would be much appreciated. Thanks, George Class Patients Private mstrName As String Private mstrOperationDescription As String Private mlngSurgeon As Long Private mdblOpDuration As Double Public Property Get Name() As String Name = mstrName End Property Public Property Get OperationDescription() As String OperationDescription = mstrOperationDescription End Property Public Property Get Surgeon() As Long Surgeon = mlngSurgeon End Property Public Property Get OpDuration() As Double OpDuration = mdblOpDuration End Property Class Schedule Private mlngDay As Long Private mdblTotalDuration As Double Public Property Get Day() As Long Day = mlngDay End Property Public Property Let Day(ByVal lDay As Long) mlngDay = lDay End Property Public Property Get TotalDuration() As Double TotalDuration = mdblTotalDuration End Property Public Property Let TotalDuration(ByVal dTotalDuration As Double) mdblTotalDuration = dTotalDuration End Property Test Sub calculating the total duration but I am not able to list them according to days count Public Sub Test() Dim mydata As New clsData Dim schedule1 As New clsSchedule Dim schedule2 As New clsSchedule Dim i As Integer mydata.InputData For i = 1 To mydata.PatientCount If mydata.patient(i).Surgeon = 1 Then schedule1.TotalDuration = schedule1.TotalDuration + mydata.patient(i).OpDuration Else schedule2.TotalDuration = schedule2.TotalDuration + mydata.patient(i).OpDuration End If Next i MsgBox "Total Duration is: " & schedule1.TotalDuration End Sub
One solution would be to have a Collection of Surgeon objects and a Collection of Patient objects. Each Patient has a Surgeon as a property, and each Surgeon has a Collection of Patients. From this you can find out the total operation duration across all patients belonging to a Surgeon. The trick is to know how the objects relate to each other - in this case each Patient has one Surgeon, and each Surgeon can have many Patients. This leads quite logically to the following structure: Patient Private mstrName As String Private mstrOperationDescription As String Private mobjSurgeon As cSurgeon Private mdblOpDuration As Double Property Let Name(txt As String) mstrName = txt End Property Property Let OperationDescription(txt As String) mstrOperationDescription = txt End Property Property Let Surgeon(objSurgeon As cSurgeon) Set mobjSurgeon = objSurgeon End Property Property Let OpDuration(num As Double) mdblOpDuration = num End Property Property Get OpDuration() As Double OpDuration = mdblOpDuration End Property Surgeon Private mstrName As String Private mlngSurgeonId As Long Private mcolPatients As Collection Private Sub Class_Initialize() Set mcolPatients = New Collection End Sub Property Let Name(txt As String) mstrName = txt End Property Property Get Name() As String Name = mstrName End Property Property Let IdNumber(num As Long) mlngSurgeonId = num End Property Property Get IdNumber() As Long IdNumber = mlngSurgeonId End Property Sub AddPatient(objPatient As cPatient) mcolPatients.Add objPatient, objPatient.Name End Sub Function TotalHours() As Double Dim objPatient As cPatient For Each objPatient In mcolPatients TotalHours = TotalHours + objPatient.OpDuration Next objPatient End Function Test Routine Sub CheckSurgeonHours() Dim colSurgeons As Collection Set colSurgeons = New Collection Dim colPatients As Collection Set colPatients = New Collection 'Populate Surgeon and Patient collections from input data 'This is the static data for each object, i.e. name, Id, operation type/duration Dim objSurgeon As cSurgeon For Each objSurgeon In colSurgeons Dim objPatient As cPatient For Each objPatient In colPatients objPatient.Surgeon = objSurgeon objSurgeon.AddPatient objPatient Next objPatient Debug.Print objSurgeon.TotalHours Next objSurgeon End Sub
subobjects of class module not working in vba for excel
I've searched for a bit, but I couldn't find a similar enough question/answer. So here it goes: I have a class object called Project. A Project can have multiple Scenarios assoicated with it. I've created the class modules for each object. But I am having difficulty in, I believe, instantiating the Scenarios collection for a given Project. Here are the class modules: 1) cProject: Private pProjectID As Integer Private pName As String Private pDateCreated As String Private pScenarios As cScenarios ' PROPERTIES Public Property Get ProjectID() As Integer ProjectID = pProjectID End Property Public Property Let ProjectID(value As Integer) pProjectID = value End Property Public Property Get name() As String name = pName End Property Public Property Let name(value As String) pName = value End Property Public Property Get Scenarios() As cScenarios Set Scenarios = pScenarios End Property Public Property Set Scenarios(value As cScenarios) Set pScenarios = value End Property 2) cScenarios collection class module: Private pScenarios As Collection Private Sub Class_Initialize() Set pScenarios = New Collection End Sub Private Sub Class_Terminate() Set pScenarios = Nothing End Sub Public Function Item(index As Variant) As cScenario Set Item = pScenarios.Item(index) End Function Public Property Get Count() As Long Count = pScenarios.Count End Property Public Sub Add(obj As cScenario) pScenarios.Add obj End Sub Public Sub Remove(index As Variant) pScenarios.Remove index End Sub And finally (3) the Scenario class object: Private pScenarioID As Integer Private pName As String Private pDateCreated As String Private pParent As cProject Public Property Get ScenarioID() As Integer ScenarioID = pScenarioID End Property Public Property Let ScenarioID(value As Integer) pScenarioID = value End Property Public Property Get name() As String name = pName End Property Public Property Let name(value As String) pName = value End Property Public Property Get parent() As cProject parent = pParent End Property Public Property Let parent(value As cProject) pParent = value End Property Here is a standard module: Sub test1() Dim cS As cScenarios Dim s As cScenario Set cS = New cScenarios For i = 1 To 3 Set s = New cScenario s.name = "s" & i cS.Add s Next Debug.Print cS.Item(3).name Debug.Print cS.Count End Sub This works. All is good. For now. I am able to populate cS with multiple scenarios. However, if I reference the scenarios collection as a child object of the project (see below in test2() ), I get a "Run-time error '91': Object variable or With block variable not set" triggered on the cs.Add call. Sub test2() Dim p As cProject Dim cS As cScenarios Dim s As cScenario Set p = New cProject Set cS = p.Scenarios For i = 1 To 3 Set s = New cScenario s.name = "s" & i cS.Add s Next Debug.Print cS.Item(3).name Debug.Print cS.Count End Sub What did I do wrong building my class modules and/or how do I fix it? Thanks.
You are not initializing pScenarios in the cProject class before trying to access it with Add(). You can fix that by adding an initializer to cProject: Private Sub Class_Initialize() Set pScenarios = New cScenarios End Sub This will guarantee that the cS instance will not be Nothing when you try to invoke Add on it inside test2. Another way (weaker IMO) would be to set p.Scenarios = new cScenarios after newing up p inside test2. Also, make sure that the property setter for cScenario.parent is Property Set instead of Property Let.
Dims being used by .class File
I have this and I want to use something similar to the way Java uses .class files to be able to call events and use them in my main code. The problem is that I cannot get the .class file to use my Dims Form1.vb: Namespace LFS_External_Client Public Class Form1 Inherits Form Private OutGauge As OutGaugeInterface Dim SpeedPref As String Dim FuelCapacity As String Dim Fuel As String Public Sub New() InitializeComponent() End Sub Private Sub Form1_Load() Handles MyBase.Load Some Code GetFuel() End Sub End Class End Namespace Then in the Dataproccer.vb (.class file): Public Class DataProcesser Public Sub GetFuel() Some Code Fuel = og.Fuel.ToString() * FuelCapacity End Sub End Class Code was shortened but has all of the relevant and necessary parts.
If you want to use the actual variables from the form instead of passing them through the method calls, you would need to declare them public instead of using dim: ... Private OutGauge As OutGaugeInterface Public SpeedPref As String Public FuelCapacity As String Public Fuel As String ...
Dim FuelCapacity As String Private Sub Form1_Load() Handles MyBase.Load Some Code DataProcesser.GetFuel(FuelCapacity) End Sub Public Shared Sub GetFuel(Byval FuelCapacity as string) Some Code Fuel = og.Fuel.ToString() * FuelCapacity End Sub
Looking at the MSDN page for the Dim Statement. It states: Code outside a class, structure, or module must qualify a member variable's name with the name of that class, structure, or module. Code outside a procedure or block cannot refer to any local variables within that procedure or block. Also according to this MSDN article the default access level for the Dim Statement is Private at the Module Level. So why not make GetFuel a function and pass the FuelCapacity in like #kcBeard states and return the Fuel value. Private Sub Form1_Load() Handles MyBase.Load Some Code Fuel = DataProcesser.GetFuel(FuelCapacity) End Sub Public Shared Function GetFuel(Byval FuelCapacity as string) as string Some Code return og.Fuel.ToString() * FuelCapacity End Function
You can make SpeedPref, FuelCapacity and Fuel public member variables, however a better approach would be to make them properties on the class with appropriate getters and setters. Dim just declares a variable. Please see the modified code sample below: Form1.vb: Namespace LFS_External_Client Public Class Form1 Inherits Form Private OutGauge As OutGaugeInterface Private _SpeedPref As String Private _FuelCapacity As String Private _Fuel As String Public Property SpeedPref Get return _SpeedPref End Get Set(value As String) _SpeedPref = value End Set End Property ... End Class End Namespace