TL; DR:
Вам нужна функция, которая может проверить, содержит ли данная строка один из нескольких запрещенных символов.Один из способов сделать это состоит в том, чтобы определить строку, содержащую каждый из этих символов по одному разу, затем выполнить итерацию каждого символа в этой строке и проверить, содержит ли ввод этот символ - и выручить, как только мы узнаем, каков ответ:
Private Function IsValidPathPartString(ByVal value As String) As Boolean
'a string is valid if it contains no characters illegal in a path/file name
Const illegalChars = "/\:*?""<>|"
Dim i As Long
For i = 1 To Len(illegalChars)
If InStr(value, Mid$(illegalChars, i, 1)) > 0 Then
Exit Function 'implicit: false
End If
Next
IsValidPathPartString = True
End Function
Model-View-Presenter
Любой нетривиальный диалог без структуры быстро превращается в большую кучу спагетти-беспорядка - независимо от того, написан он новичками VBA с MSForms или C #профессионалы с WinForms или WPF (современные рамки пользовательского интерфейса).Проблема присуща самой природе программирования пользовательского интерфейса, и программисты VBA, незнакомые с объектно-ориентированным программированием, наиболее уязвимы перед ловушками «интеллектуального пользовательского интерфейса», когда форма запускает шоу и происходит все, что когда-либо должно происходить.через форму: никогда не поздно отучиться от этого.
Работа формы заключается в том, чтобы собирать пользовательские данные и представлять данные .Период, конец этого.Откуда приходят данные, которые он представляет, это не его дело.То, что происходит с данными, собранными после сбора, также не имеет отношения к делу.
«Умный интерфейс» - не единственный способ программирования пользовательского интерфейса.Когда что-то выходит за рамки тривиального - и ваша форма определенно входит в эту нетривиальную категорию, нам нужна гораздо более надежная структура, чтобы вещи быстро не вышли из-под контроля.
Вот, Модель-View-Presenter Шаблон пользовательского интерфейса, который очень хорошо работает с MSForms (и WinForms, если вы погрузитесь в .NET-land).
Model
Есть класс "модель", чейответственность заключается в определении данных, которыми будет манипулировать форма.Этот класс модели также может быть ответственным за знание того, являются ли данные, которые он инкапсулирует, допустимыми;в более сложных сценариях вы можете разделить валидацию на собственный набор объектов, но давайте сделаем все просто.Класс может выглядеть примерно так (обратите внимание на выделенную функцию, проверяющую, содержит ли строка недопустимые символы в именах путей / файлов):
'EquipmentRequestModel.cls
Option Explicit
Private ValidationErrors As Collection
Public RequestedBy As String
Public OnSiteContact As String
Public OnSiteNumber As String
Public EventName As String
Public LocationNumber As String
Public OffSiteDelivery As String
'...
Public Property Get IsValid() As Boolean
Validate
IsValid = ValidationErrors.Count = 0
End Property
Public Property Get ModelValidationErrors As Variant
If ValidationErrors.Count = 0 Then Exit Property 'implicit vbEmpty
ReDim errors(0 To ValidationErrors.Count - 1)
Dim e As Long
For e = 0 To ValidationErrors.Count - 1
errors(e) = ValidationErrors(e + 1) 'collection indexing is 1-based
Next
ModelValidationErrors = errors
End Property
Private Sub Validate()
Set ValidationErrors = New Collection
If Not IsValidRequiredString(RequestedBy) Then OnMissingRequiredFieldError "RequestedBy"
If Not IsValidRequiredString(OnSiteContact) Then OnMissingRequiredFieldError "OnSiteContact"
If Not IsValidRequiredString(OnSiteNumber) Then OnMissingRequiredFieldError "OnSiteNumber"
If Not IsValidRequiredString(EventName) Then OnMissingRequiredFieldError "EventName"
If Not IsValidPathPartString(EventName) Then OnValidationError "Field [EventName] cannot contain characters: [/\:*?""<>|]."
'...
End Sub
Private Function IsValidRequiredString(ByVal value As String) As Boolean
'a required string is valid if it's non-empty after stripping leading/trailing spaces
IsValidRequiredString = Trim(value) <> vbNullString
End Function
Private Function IsValidPathPartString(ByVal value As String) As Boolean
'a string is valid if it contains no characters illegal in a path/file name
Const illegalChars = "/\:*?""<>|"
Dim i As Long
For i = 1 To Len(illegalChars)
If InStr(value, Mid$(illegalChars, i, 1)) > 0 Then
Exit Function 'implicit: false
End If
Next
IsValidPathPartString = True
End Function
Private Sub OnMissingRequiredFieldError(ByVal propertyName As String)
OnValidationError "Required field [" & propertyName & "] is empty."
End Sub
Private Sub OnValidationError(ByVal message As String)
ValidationErrors.Add message
End Sub
Более сложные проверки, условно зависящие от значения того или иного свойства, могут легкобыть реализованным там, и если представлению требуются дополнительные метаданные, чтобы контролировать, должно ли быть видно то или иное поле, модель может также предоставить для этого Boolean
свойств.
Теперь вы можете иметь код, который читаетна листе и заполняет свойства экземпляра этого класса значениями ячеек, или вы можете иметь код, который считывает свойства класса и заполняет их ячейками листа - хотя такой код не относится к модели.
НоКак такой класс влияет на выделение кода формы?
Представление
Форма требует ссылки на модель, в самом начале.Что приятно в MSForms, так это то, что вы бесплатно получаете атрибут VB_PredeclaredId
(его легко злоупотреблять , но это другое обсуждение), поэтому очень легко добавить фабричный метод Create
именно это и делает:
'EquipmentRequestView.frm
Option Explicit
Private model As EquipmentRequestModel
Public Property Get EquipmentRequestModel() As EquipmentRequestModel
Set EquipmentRequestModel = model
End Property
Public Property Set EquipmentRequestModel(ByVal value As EquipmentRequestModel)
Set model = value
LoadModelData
End Property
Public Function Create(ByVal viewModel As EquipmentRequestModel) As EquipmentRequestView
Dim result As EquipmentRequestView
Set result = New EquipmentRequestView
Set result.EquipmentRequestModel = viewModel
Set create = result
End Function
Private Sub LoadModelData()
'synchronize control values as per model
Me.TextBoxE_RequestBy.Value = model.RequestBy
Me.TextBoxE_OnSiteContact.Value = model.OnSiteContact
'...
ValidateForm
End Sub
Форма содержит несколько элементов управления, и все эти элементы управления могут реагировать на события.Поэтому мы обрабатываем событие Change
этих элементов управления и соответствующим образом обновляем модель:
Private Sub TextBoxE_RequestBy_Change()
model.RequestBy = Me.TextBoxE_RequestBy.Value
ValidateForm
End Sub
Private Sub TextBoxE_OnSiteContact_Change()
model.OnSiteContact = Me.TextBoxE_OnSiteContact.Value
ValidateForm
End Sub
'...
Процедура ValidateForm
, вызываемая из соответствующего обработчика каждого элемента управления, каждый пользовательский ввод вызывает модельПодлежит проверке:
Private Sub ValidateForm()
Dim isValidForm As Boolean
isValidForm = model.IsValid
'command buttons are only enabled if form is valid
Me.E_EnterInformation.Enabled = isValidForm
Me.CommandButton_ECancelREV.Enabled =isValidForm
'validation errors label is only visible with invalid data
Me.ValidationErrorsLabel.Visible = Not isValidForm
Me.ValidationErrorsLabel.Caption = Join(model.ModelValidationErrors, vbNewLine)
End Sub
Вы могли бы также иметь более детальные метаданные об ошибках валидации с более продуманным механизмом валидации данных.Например, вместо простых строк ошибкой проверки модели может быть сам по себе объект со свойствами ErrorMessage
, ViewControlName
и ModelPropertyName
, которые облегчают прикрепление определенной ошибки проверки к конкретному элементу управления нанапример, если вы хотите выделить красным цветом нужное поле, сфокусировать его или переключить видимость симпатичного маленького красного значка «X» с сообщением vaildation в свойстве значка ControlToolTip
- предел небаздесь.
Что касается формы / представления, то это все.Это будет обработчик Click
для вашей кнопки E_EnterInformation
:
Private Sub E_EnterInformation_Click()
Me.Hide
End Sub
Единственное, чего не хватает, - это обработки события QueryClose
, чтобы мы могли отследить, что пользователь хочет просто внести в залог.вне формы и притворяться, что они никогда не хотели поднимать его.
Так как же тогда данные формы попадают на лист?
Presenter
Другой класс должен нести ответственность за соединение точек: что-то где-то должно создать модель, инициализировать ее (при необходимости), создать форму / передать ей модель, а затем показать форму и определить, что делать с теперь действующей версией.данные модели.
'EquipmentRequestPresenter.cls
Option Explicit
Public Sub Run()
Dim model As EquipmentRequestModel
Set model = InitializeModel
With EquipmentRequestView.Create(model)
.Show
'todo: handle a user-cancelled form?
UpdateWorksheet model
End With
End Sub
Private Function InitializeModel() As EquipmentRequestModel
Dim model As EquipmentRequestModel
Set model = New EquipmentRequestModel
'note: should probably be "With EquipmentRequestSheet"
With ActiveWorkbook.Worksheets("Equipment Request")
model.RequestBy = .Range("C6").Value 'todo: name these ranges...
model.OnSiteContact = .Range("C7").Value '...urgently...
model.OnSiteNumber = .Range("C8").Value '...before someone inserts a row/column
'...
End With
Set InitializeModel = model
End Function
Private Sub UpdateWorksheet(ByVal model As EquipmentRequestModel)
'note: should probably be "With EquipmentRequestSheet"
With ActiveWorkbook.Worksheets("Equipment Request")
.Unprotect
.Range("C6").Value = model.RequestBy
.Range("C7").Value = model.OnSiteContact
.Range("C8").Value = model.OnSiteNumber
'...
.Protect
End With
End Sub
Обратите внимание, что это означает, что процедура ESaveBook
теперь также может принимать параметр model
и использовать model.DeliveryDate
вместо Range("C10")
- что делает ее на одно место меньше для беспокойствакогда нужно изменить шаблон листа и добавить строку вверху, или столбец смещает все эти координаты ячейки и по-королевски все портит.Использование .Range("DeliveryDate")
уже защитит ваш код от этого: именованный диапазон становится слоем абстракции между вашим кодом и фактическими ячейками рабочего листа, так что фактические координаты абстрагируются.из кода, который больше не нуждается во всевозможных «зачем снова эта ячейка?»комментарии ... которые могут быть или не быть точными.
Также обратите внимание: PascalCase
регистр в модуле и процедуре на простом английском языке, произносимые имена, нигде не подчеркивание, нет прикольных префиксов.Не уверен, в чем дело с E
везде.
В любом случае, при такой настройке макрос, который в данный момент вызывает .Show
в этой форме, теперь должен выглядеть следующим образом:
Public Sub MyMacro()
With New EquipmentRequestPresenter
.Run
End With
End Sub
Заключительное примечание: все вышеперечисленное - это воздушный код, предоставленный для иллюстрации концепций;ничего из этого не было проверено.