combining classes in vba? - class

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

Related

reference subform of subform from 4th form

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.

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.

Class of a Class

I'm getting just killed trying to make a class of a class. I have shopped around the site and seen several examples but maybe because its 1:43 I am having a hard time understanding them.
I was successfully able to use a class to automate a huge data entry project at work. I created a class called catDist which is the category distribution of types of agricultural products a company could manufacture or sell.
catDist contains six properties:
Private selfWorth As String
Private Q1 As Double
Private Q2 as Double
Private Q3 as Double
Private Q4 As Double
Private activated As Boolean
They all have the standard get and let codes.
There are 48 possible categories. I have a module that creates 48 instances of them with 48 different values for selfWorth (e.g "Cottonseed", or "maize" etc), and sets Q1 through Q4 as 0 . The module originally worked with a Userform that I could type in the values and hit enter. If it saw that I had entered a value inside a particular textbox (yes there were 48X4 textboxes) it would set activated to true and changes the relevant Q's to the values I entered.
WHAT I WANT TO DO NOW.
It was a great success. Now what I want to do is create a class called "Distributor". Each distributor class would have 4 collections have catDist objects. I can create the distributor class. I can create the catDist class. But for the love of God I can not figure out a way to set the corresponding distributor catDist property to the catDist value I used in the Set method.
Sub testRegist()
Dim registrant As testRegistrant
Set registrant = New testRegistrant
registrant.registNum = "Z000123"
'MsgBox (registrant.registNum)
Dim cowMilk As testcatDist
Set cowMilk = New testcatDist
cowMilk.selfWorth = "Cow Milk"
cowMilk.distribution = 4.6
registrant.testCat = cowMilk
Debug.Print registrant.testCat.selfWorth
End Sub
catDist Class
Private pselfWorth As String
Private pdistribution As Double
Public Property Get selfWorth() As String
selfWorth = pselfWorth
End Property
Public Property Let selfWorth(name As String)
pselfWorth = name
End Property
Public Property Get distribution() As Double
distribution = pdistribution
End Property
Public Property Let distribution(dist As Double)
pdistribution = dist
End Property
Registrant a.k.a distributor class
Private pRegistNum As String
Private pCatDist As testcatDist
Public Property Get registNum() As String
registNum = pRegistNum
End Property
Public Property Let registNum(registration As String)
pRegistNum = registration
End Property
Public Property Get testCat() As testcatDist
testCat = pCatDist
End Property
Public Property Let testCat(cat As testcatDist)
Set pCatDist = New testcatDist
pCatDist = cat
End Property
The only problem I see is that you are using Let instead of Set. In VBA you use Set when assigning to objects.
When you write registrant.testCat = cowMilk (in your Sub), testCat = pCatDist (in the getter of testRegistrant.testCat) and pCatDist = cat (in the setter of testRegistrant.testCat) you are implicitly using Let (it's as if you had written Let registrant.testCat = cowMilk) instead of (explicitly) using Set.
So, if you write Set registrant.testCat = cowMilk in your test Sub, Set testCat = pCatDist in the getter and Set pCatDist = cat in the setter you should be good to go.
Also, in the same setter, the initialization of pCatDist isn't needed since you are passing cat to it in the next line.
And, as #GSerg (thank you) says, the signature of your setter should be Public Property Set testCat(cat as testcatDist) instead of Public Property Let.

Explicit Loading of child navigation properties with criteria

Using DBContext in EF5 - after filtering and partial loading based on criteria like a date range.
I'm trying to produce a complete graph or tree of objects - Persons->Events where the only Events that are included are within a date range. All this whilst preserving the standard change tracking that one gets with the following:
Dim Repository As Models.personRepository = New Models.personRepository
Private Sub LoadData()
Dim personViewModelViewSource As System.Windows.Data.CollectionViewSource = CType(Me.FindResource("personViewModelViewSource"), System.Windows.Data.CollectionViewSource)
Repository.endDate = EndDate.SelectedDate
Repository.startDate = StartDate.SelectedDate
personViewModelViewSource.Source = Repository.collectionOfpersons
End Sub
A listbox and a datagrid are both bound as a proper datasource. The POCO template has been modified to put INotifyProperty events in the navigation property class, Events.
I've been fighting with this for days now and filtering whether on Lazy loading or Explicit loading does not function. After masses of blog/chaper reading, I'm aware of the rather unreal limitation relating to Include; instead I'm trying explicit loading. I'm using the DBContext book btw.
Being unable to bring back only a subset of the Event data from the database is 100% deal breaker as there is likely to be hundreds of thousands of Events per Person. It doesn't make sense to me or my boss that the Entity Framework doesn't make this functionality fairly obvious-are we missing something?
I've left in the commented code to try and illustrate some of the paths I've attempted. The class itself is a repository that this method belongs to. I'll edit this question further to clarify just how many routes I've tried as it's been a LOT. The View uses a repository layer and a ViewModel so the code behind on the XAML is rather minimal.
In advance for any help, thank you!
Public Overridable ReadOnly Property AllFiltered(startdate As Date, enddate As Date) As ObservableCollection(Of person) Implements IpersonRepository.AllFiltered
Get
Dim uow = New UnitOfWork
context = uow.Context
Dim personQuery = context.persons.Include(Function(p) p.events).AsQueryable.Where(Function(x) x.personID = 10).FirstOrDefault
'Dim eventQuery = From e In context.Notes
' Where e.eventDateTime >= startdate And e.eventDateTime <= enddate
' Select e
'Dim personQuery As person = From r In context.persons
' From e In eventQuery
' Where r.personID = e.personID
' Select r, e
Dim singleperson = personQuery
'For Each r As person In personQuery
' persons.Add(r)
'Next
' context.Entry(eventQuery).Collection()
' context.Entry(personQuery).Reference(personQuery).Load()
context.Entry(singleperson).Collection(Function(d) d.events).Query().Where(Function(x) x.eventDateTime > startdate And x.eventDateTime < enddate).Load()
Return context.persons.Local
End Get
End Property
Note: I'm using logging/exception handling via PostSharp rather than polluting the code.
Below are some of the errors I've generated with previous paths taken.
The entity type DbQuery`1 is not part of the model for the current context.
The Include path expression must refer to a navigation property defined on the type. Use dotted paths for reference navigation properties and the Select operator for collection navigation properties.
Parameter name: path
Unable to cast object of type 'System.Data.Entity.Infrastructure.DbQuery1[VB$AnonymousType_02
[Entity.Person,Entity.Notes]]' to type 'System.Collections.ObjectModel.ObservableCollection`1[Entity.Person]'.
UPDATE: Yet another route I've tried, still cannot get this to fly either:
Private Property _collectionOfPersons As ObservableCollection(Of Person)
Public ReadOnly Property collectionOfPersons As ObservableCollection(Of Person)
Get
For Each Person In context.Persons
_collectionOfPersons.Add(ReturnSinglePerson(startDate, endDate, Person.PersonID))
Next
Return _collectionOfPersons.Where(Function(x) x.events.Where(Function(e) e.eventDateTime > startDate And e.eventDateTime < endDate))
End Get
End Property
Public Overridable ReadOnly Property SinglePerson(startdate As Date, enddate As Date) As ObservableCollection(Of Person) Implements IPersonRepository.AllFiltered
Get
Dim PersonQuery = context.Persons.Include(Function(p) p.events).AsQueryable.Where(Function(x) x.PersonID = 10).Select(Function(x) x).FirstOrDefault
Dim Person = PersonQuery
context.Entry(Person).Collection(Function(d) d.events).Query().Where(Function(x) x.eventDateTime > startdate And x.eventDateTime < enddate).Load()
Return context.Persons.Local
End Get
End Property
Public Function ReturnSinglePerson(startdate As Date, enddate As Date, id As Integer)
Dim PersonQuery = context.Persons.Include(Function(p) p.events).AsQueryable.Where(Function(x) x.PersonID = id).Select(Function(x) x).FirstOrDefault
Dim Person = PersonQuery
Return Person
End Function
Another shot:
Public Overridable ReadOnly Property FilteredPersons(startdate As Date, enddate As Date) As ObservableCollection(Of Person) Implements IPersonRepository.AllFiltered
Get
context.Persons.Load()
Dim DateCriteria = Function(e) e.events.Where(Function(d) d.eventDateTime > startdate And d.eventDateTime < enddate)
Dim Res = New ObservableCollection(Of Person)
For Each Person In context.Persons.Local.Select(Function(x) x).Where(DateCriteria)
Res.Add(Person)
Next
Return Res
End Get
End Property
Gives:
Public member 'Where' on type 'ObservableCollection(Of DailyNotes)' not found.
Tantalisingly close, only I get lots of duplicate names on the listbox - but the navigation carries through and the Date criteria work.
<ExceptionAspect>
Public Overridable ReadOnly Property FilteredPersons(startdate As Date, enddate As Date) As ObservableCollection(Of Person) Implements IPersonRepository.AllFiltered
Get
context.Persons.Load()
Dim test = From r In context.Persons
From e In context.Notes
Where e.eventDateTime > startdate And e.eventDateTime < enddate
Join rr In context.Persons On e.PersonID Equals rr.PersonID
Select r, e
Dim Res = New ObservableCollection(Of Person)
For Each Person In test
Res.Add(Person.r)
Next
Return Res
End Get
End Property
Don't try this one :). It simply selects the child properties only.
Public ReadOnly Property collectionOfResidents As ObservableCollection(Of resident)
Get
For Each resident In context.residents
_collectionOfResidents.Add(ReturnSingleResident(startDate, endDate, resident.residentID))
Next
Return _collectionOfResidents.Select(Function(x) x.events.Where(Function(e) e.eventDateTime > startDate And e.eventDateTime < endDate))
End Get
End Property
I'm hoping that adding my other attempts to this question may prompt both other answers and help others see the circles they can get into when first tackling this!
You can use the Select clause for finer control than with Include
Something like this:
context
.Persons
.Where( ... some predicate on Person ... )
.Select( o => new
{
Person = o,
Events = o.Events.Where( ... some predicate on Event ... )
}
)
;
This will translate both predicates into SQL which execute on the database server.
Ok after a LOT of fiddling and misunderstanding of anonymous types this evening, I think I succeeded. Nicholas's answer just needed to be done in VB which took me a while - I've not used anonymous types before.
This is what appears to work fine in my repository layer:
<ExceptionAspect>
Public Overridable ReadOnly Property FilteredPersons(startdate As Date, enddate As Date) As ObservableCollection(Of Person) Implements IPersonRepository.AllFiltered
Get
context.Persons.Load()
Dim test = context.Persons.Where(Function(r) r.PersonActive).Select(Function(o) New With { _
.Person = o, _
.events = o.events.Where(Function(e) e.eventDateTime > startdate) _
})
Dim PersonList= New ObservableCollection(Of Person)
For Each t In test
PersonList.Add(t.person)
Next
Return PersonList
End Get
End Property
The crucial updating/saving in the wpf View is intact, I'm really happy and grateful to Nicholas for his help on here (and patience...re:cartesion product). So thank you. I hope this helps someone else!

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.