VBA Raising событие в другом классе - PullRequest
1 голос
/ 09 января 2020

Я пытаюсь реализовать предложения из этого поста в codereview

Цель:

Управление тем, что происходит, когда пользователи взаимодействуют с таблицами Excel (ListObjects)

Идея в конце состоит в том, чтобы иметь собственные события для разных таблиц. Например, когда вы добавляете строку в table1, вызывается пользовательский AddEvent1, а когда вы делаете то же самое для table2, вызывается AddEvent2.

Будет только один класс для управления событиями и один для хранения таблиц и их информация.


Таким образом, предлагается следующий процесс:

  1. Добавить объект списка в класс с именем Table
  2. Этот класс будет прослушивать события на родительском листе (Change и SelectionChange)
  3. Когда происходит событие изменения, вызвать пользовательское событие из класса TableManager, который обрабатывает эти события (такие события, как adding, updating или deleting строк)

РЕДАКТИРОВАТЬ # 1:

Скорректирован код:

  • Функция Create теперь возвращает экземпляр Table
  • , а свойство Set SourceTable теперь устанавливает для поля listObjectParentSheet соответствующее значение

Но все же Table Manager не слушает событие, возбужденное в listObjectParentSheet_Change


Компоненты:

1) Лист с Таблица Excel (ListObject) и следующий код:

Private Sub Worksheet_Activate()

    Dim myTable As Table
    Dim myTableManager As TableManager

    Set myTable = Table.Create(Me.ListObjects(1))

    Set myTableManager = New TableManager

    Set myTableManager.TableInstance = myTable

End Sub

2) Класс Table (с предварительно установленным идентификатором, установленным в true, с использованием rubberduck )

'@Folder("VBAProject")

Option Explicit
'@PredeclaredId

Private Type TTable
    SourceTable As ListObject
End Type

Private this As TTable

Private WithEvents listObjectParentSheet As Excel.Worksheet

Public Event AddEvent()

Public Property Get SourceTable() As ListObject
    Set SourceTable = this.SourceTable
End Property

Public Property Set SourceTable(ByVal value As ListObject)
    Set this.SourceTable = value
    Set listObjectParentSheet = value.Parent
End Property

Public Property Get Self() As Table
    Set Self = Me
End Property

Public Function Create(ByVal EvalSourceTable As ListObject) As Table
    With New Table
        Set .SourceTable = EvalSourceTable
        Set Create = .Self
    End With
End Function

Private Sub listObjectParentSheet_Change(ByVal Target As Range)
    If Not Intersect(Target, SourceTable.DataBodyRange) Is Nothing Then
        MsgBox listObjectParentSheet.Name & " " & Target.Address
        RaiseEvent AddEvent
    End If
End Sub

3) Класс TableManager

Option Explicit

Private WithEvents m_table As Table

Public Property Get TableInstance() As Table
    Set TableInstance = m_table
End Property

Public Property Set TableInstance(ByRef tableObject As Table)
    Set m_table = tableObject
End Property

Private Sub m_table_AddEvent()
    MsgBox "Adding something"
End Sub

Вопрос / выпуск:

Я не понял, как запустить AddEvent "в классе TableManager. Я знаю, что перепутал некоторые концепции создания классов, но я не знаю, что я делаю неправильно.


Ожидаемый результат:

Когда пользователь изменяет любую ячейку объекта списка, показывает окно сообщения «Добавление чего-либо», когда AddEvent поднимается


Любая помощь будет очень признательна.

EDIT # 2

Окончательный код благодаря ответу Мэта:

Лист: Sheet1:

Private Sub Worksheet_Activate()
    With TableManager
        Set .TableEvents = Table.Create(Sheet1.ListObjects(1))
    End With
End Sub

Модуль: ListObjectUtilities

Option Explicit

Public Function GetCellRow(ByVal EvalTable As ListObject, ByVal EvalCell As Range) As Long

    If Intersect(EvalCell, EvalTable.DataBodyRange) Is Nothing Then Exit Function

    GetCellRow = EvalCell.Row - EvalTable.HeaderRowRange.Row

End Function

Public Function GetCellColumn(ByVal EvalTable As ListObject, ByVal EvalCell As Range) As Long

    If Intersect(EvalCell, EvalTable.DataBodyRange) Is Nothing Then Exit Function

    GetCellColumn = EvalCell.Column - EvalTable.HeaderRowRange.Column + 1

End Function

Класс: ITable

Option Explicit

Public Property Get SourceTable() As ListObject
End Property

Класс: Table

'@Folder("VBAProject")
'@PredeclaredId
Option Explicit

Private WithEvents TableSheet As Excel.Worksheet

Private Type TTable
    SourceTable As ListObject
    LastRowCount As Long
    LastColumnCount As Long
End Type

Private this As TTable

Public Event Changed(ByVal cell As Range)
Public Event AddedNewRow(ByVal newRow As ListRow)
Public Event AddedNewColumn(ByVal newColumn As ListColumn)

Implements ITable

Public Function Create(ByVal Source As ListObject) As ITable
    With New Table
        Set .SourceTable = Source
        Set Create = .Self
    End With
End Function

Public Property Get Self() As Table
    Set Self = Me
End Property

Public Property Get SourceTable() As ListObject
    Set SourceTable = this.SourceTable
End Property

Public Property Set SourceTable(ByVal value As ListObject)
    ThrowIfSet this.SourceTable
    ThrowIfNothing value
    Set TableSheet = value.Parent
    Set this.SourceTable = value
    Resize
End Property

Friend Sub OnChanged(ByVal Target As Range)
    RaiseEvent Changed(Target)
End Sub

Friend Sub OnAddedNewRow(ByVal newRow As ListRow)
    RaiseEvent AddedNewRow(newRow)
End Sub

Friend Sub OnAddedNewColumn(ByVal newColumn As ListColumn)
    RaiseEvent AddedNewColumn(newColumn)
End Sub

Private Sub ThrowIfNothing(ByVal Target As Object)
    If Target Is Nothing Then Err.Raise 5, TypeName(Me), "Argument cannot be a null reference."
End Sub

Private Sub ThrowIfSet(ByVal Target As Object)
    If Not Target Is Nothing Then Err.Raise 5, TypeName(Me), "This reference is already set."
End Sub

Private Sub Resize()
    With this.SourceTable
        this.LastRowCount = .ListRows.Count
        this.LastColumnCount = .ListColumns.Count
    End With
End Sub

Private Sub TableSheet_Change(ByVal Target As Range)

    If Intersect(Target, SourceTable.DataBodyRange) Is Nothing Then Exit Sub

    Select Case True
    Case this.SourceTable.DataBodyRange.Columns.Count > this.LastColumnCount
        OnAddedNewColumn SourceTable.ListColumns(ListObjectUtilities.GetCellColumn(this.SourceTable, Target))
    Case this.SourceTable.DataBodyRange.Rows.Count > this.LastRowCount
        OnAddedNewRow SourceTable.ListRows(ListObjectUtilities.GetCellRow(this.SourceTable, Target))
    Case Else
        OnChanged Target
    End Select
    Resize
End Sub

Private Property Get ITable_SourceTable() As ListObject
    Set ITable_SourceTable = this.SourceTable
End Property

Класс: TableManager

'@Folder("VBAProject")
'@PredeclaredId
Option Explicit
Private WithEvents MyTable As Table

Public Property Get TableEvents() As Table
    Set TableEvents = MyTable
End Property

Public Property Set TableEvents(ByVal value As Table)
    Set MyTable = value
End Property

Private Sub MyTable_AddedNewColumn(ByVal newColumn As ListColumn)
    MsgBox "Added new column " & newColumn.Range.Column
End Sub

Private Sub MyTable_AddedNewRow(ByVal newRow As ListRow)
    MsgBox "Added new row " & newRow.Range.Row
End Sub

Private Sub MyTable_Changed(ByVal cell As Range)
    MsgBox "Changed " & cell.Address
End Sub

Пример файла

1 Ответ

1 голос
/ 09 января 2020

Я попытался воспроизвести, но потом обнаружил, что, опираясь на Worksheet.Activate, чтобы зарегистрировать обработчик, он ведет себя неправильно: иногда вам нужно «покачивать» лист, чтобы он продолжал работать, особенно если вы редактируете код. Может быть просто так:)

Обратите внимание, что для того, чтобы иметь возможность выстрелить AddedNewRow, AddedNewColumn, или даже RemovedRow или RemovedColumn, вам необходимо постоянно отслеживать размер таблица со смесью обработчиков Worksheet.Change и Worksheet.SelectionChange.

Таблица * Модуль класса 1013 *:

'@Folder("VBAProject")
'@PredeclaredId
Option Explicit

Private WithEvents TableSheet As Excel.Worksheet

Private Type TTable
    SourceTable As ListObject
    LastRowCount As Long
    LastColumnCount As Long
End Type

Private this As TTable

Public Event Changed(ByVal cell As Range)
Public Event AddedNewRow(ByVal newRow As ListRow)
Public Event AddedNewColumn(ByVal newColumn As ListColumn)

Public Function Create(ByVal Source As ListObject) As Table
    With New Table
        Set .SourceTable = Source
        Set Create = .Self
    End With
End Function

Public Property Get Self() As Table
    Set Self = Me
End Property

Public Property Get SourceTable() As ListObject
    Set SourceTable = this.SourceTable
End Property

Public Property Set SourceTable(ByVal Value As ListObject)
    ThrowIfSet this.SourceTable
    ThrowIfNothing Value
    Set TableSheet = Value.Parent
    Set this.SourceTable = Value
    Resize
End Property

Friend Sub OnChanged(ByVal Target As Range)
    RaiseEvent Changed(Target)
End Sub

Friend Sub OnAddedNewRow(ByVal newRow As ListRow)
    RaiseEvent AddedNewRow(newRow)
End Sub

Friend Sub OnAddedNewColumn(ByVal newColumn As ListColumn)
    RaiseEvent AddedNewColumn(newColumn)
End Sub

Private Sub ThrowIfNothing(ByVal Target As Object)
    If Target Is Nothing Then Err.Raise 5, TypeName(Me), "Argument cannot be a null reference."
End Sub

Private Sub ThrowIfSet(ByVal Target As Object)
    If Not Target Is Nothing Then Err.Raise 5, TypeName(Me), "This reference is already set."
End Sub

Private Sub Resize()
    With this.SourceTable
        this.LastRowCount = .ListRows.Count
        this.LastColumnCount = .ListColumns.Count
    End With
End Sub

Private Sub TableSheet_Change(ByVal Target As Range)
    If Not (Target.ListObject Is SourceTable) Then Exit Sub
    OnChanged Target
    Resize
End Sub

Обратите внимание, что вы можете использовать оператор Is для определить, относится ли Target.ListObject к тому же объекту, что и SourceTable, вместо использования Application.Intersect с диапазонами:

If Not (Target.ListObject Is SourceTable) Then Exit Sub

Оттуда все, что нам нужно, это класс для обработки этого события Changed - I поместите это в коде Sheet1 здесь, но подойдет любой модуль класса (включая модуль UserForm):

Sheet1 модуль листа:

'@Folder("VBAProject")
Option Explicit
Private WithEvents MyTable As Table

Public Property Get TableEvents() As Table
    Set TableEvents = MyTable
End Property

Public Property Set TableEvents(ByVal value As Table)
    Set MyTable = value
End Property

Private Sub MyTable_Changed(ByVal cell As Range)
    MsgBox "Changed " & cell.Address
End Sub

Ссылка Table все еще должна быть где-то Set - здесь в обработчике Open рабочей книги хоста:

ThisWorkbook модуль рабочей книги:

'@Folder("VBAProject")
Option Explicit

Private Sub Workbook_Open()
    With Sheet1
        Set .TableEvents = Table.Create(.ListObjects(1))
    End With
End Sub

Следующим шагом будет очистка интерфейса publi c, возвращаемого Table.Create - в его нынешнем состоянии все довольно запутанно, и Table в terface немного раздут:

Public and Friend members of the Table interface

Все эти участники будут доступны для Sheet1.TableEvents, если мы не сделаем что-нибудь. Что если бы мы могли выставить членам только тот код клиента, который действительно нужен, вот так?

Only the SourceTable member is listed for the object returned by Table.Create

С Rubberduck вы можете Извлеките интерфейс , щелкнув правой кнопкой мыши в любом месте класса Table и выбрав «Извлечь интерфейс» в меню «Рефакторинг», а затем выберите элементы для извлечения - здесь получатель SourceTable (мы не собираюсь выставить сеттер!):

Rubberduck's Extract Method refactoring

Это создаст новый закрытый класс (это изменится в будущих выпусках) - сделайте его PublicNotCreatable в свойствах toolwindow (F4), если интерфейс был извлечен из класса publi c.

Рефакторинг добавит Implements ITable в начало класса Table (при условии, что вы не переименовали интерфейс), и этот член будет добавлен:

Private Property Get ITable_SourceTable() As ListObject
    Err.Raise 5 'TODO implement interface member
End Property

Все, что вам нужно сделать, это предоставить реализацию:

Private Property Get ITable_SourceTable() As ListObject
    Set ITable_SourceTable = this.SourceTable
End Property

И теперь Table.Create может вернуть ITable абстракция:

Public Function Create(ByVal Source As ListObject) As ITable
...