Cookie Cutter Forms
How do you reuse a form with external dependencies in multiple projects? Interfaces.
In previous articles, I discussed interfaces both conceptually and practically. So, we've discussed what interfaces are and how to use them in VBA. Now, let's talk about when to use them by going through a few scenarios where interfaces make sense. I'll cover the second scenario in this article.
- Backward-compatible refactoring
- Generic forms in a code library
- Dependency injection
- Cleaner IntelliSense
Generic Forms in a Code Library
I write a lot of business applications. One form that I commonly use is a scanning/document upload form. The form consists of a continuous subform on the left side and an auto-resizing image viewer control on the right side. There are buttons to zoom in and out of the image viewer. There's a button to print the image. There are buttons to navigate pages of a multi-page document.
Under the left-side subform, there are buttons to capture scans (via scanner dialog or quick buttons to scan via the flatbed or document feeder); a dropdown for setting and choosing pre-configured scan settings; buttons to rotate scanned images; and buttons to delete entire documents or individual pages.
There is a lot of functionality built into this form. More importantly, it's the kind of functionality that is constantly tweaked as it bumps into edge cases and needs to be fixed.
The above are all features that one would expect in any document management system. None of these features is application-specific. Of course, there will be some things that do get implemented differently in each application.
For example, the subform in the top-left corner of my screenshot above differs for every application. The document metadata that's important for a delinquent tax claim management system is different than what's important for an inheritance tax management system.
However, most of the generic features of the form depend on properties of the records in the subform. How can we write and maintain a generic form that has a dependency on a subform that will be different for every application? Lots and lots of copying and pasting. LOL. Just kidding. The answer is interfaces, but you knew that since this post is one in a series of posts on...interfaces.
How does this actually work? How do we go about converting our generic form to use interfaces so that our parent scan form is entirely decoupled from the subform it depends on? It's a four-step process:
- Identify the specific dependencies (properties, methods, etc.)
- Create an interface that creates a contract to provide these dependencies
- Program the generic form using objects declared as types of the interface
- Implement the interface in our application-specific subform
Of course, we only have to follow the above steps in a single application. Once we have a working prototype, we then export the generic form and its associated interfaces into our code library. Then, when we want to implement the generic form in a new application, it is a simple two-step process:
- Import the generic form and associated interfaces
- Implement the interface in our application-specific subform
Where this solution really shines is when we identify a bug in our generic form. We fix the bug in one application, export the fix into our code library, then replace any instances of the form in our other applications. We don't need to spend a bunch of time adjusting the imported form so that it works with each application. As long as everyone is abiding by the contract (i.e., the interface(s)), we can guarantee that the updated version will work everywhere.
To get a sense of how this would work, here is a copy of my iDocDetails interface class. Each application where I use my generic ScanForm must have a form named DocSF that
If you've read my other articles, you might notice that I'm using an old naming convention for my Enum type. It's there for good, though, because backwards compatibility trumps consistency.
'--------------------------------------------------------------------------------------- ' Module : iDocDetails ' Author : Mike ' Date : 10/18/2011 - 10/22/2019 12:25 ' Purpose : Interface that must be implemented by DocSF for use with ScanForm. ' Usage : In header section of Form_DocSF: ' Implements iDocDetails '--------------------------------------------------------------------------------------- Option Compare Database Option Explicit Public Enum docItemType docTIF docJPG docImport End Enum Public ScanPages As Long Public Property Get FileName() As String 'Return the filename of the source file for the current record End Property Public Property Get ItemType() As docItemType End Property Public Property Get ItemDescription() As String End Property Public Property Get ReadyToScan() As Boolean End Property Public Property Get CanDelete() As Boolean 'Return True if the user has rights to delete the document ' (applies to both the document itself and the individual pages) End Property Public Property Let FileExt(ByVal sFileExt As String) 'This is the file extension of an imported file; it gets set by ScanForm 'After updating the record, remember to set Me.Dirty = False End Property Public Property Let SrcFileName(ByVal sSrcFileName As String) 'This is the file name (minus extension and folder path) of an imported file; ' it can be used as an item description if the user has not already provided one 'After updating the record, remember to set Me.Dirty = False End Property Public Function DeleteItem() As Boolean 'This is the function called when the user presses the Delete button on the Scan form; ' the scan form will take care of confirming the deletion, but the DocSF must delete ' the record itself; the return value is whether the item was successfully deleted (True) or not (False) 'If the record is successfully deleted (ie, DeleteItem()=True), ' the scan form will delete the underlying file, if one exists End Function Public Function TwainX() As Object 'To use the TwainControlX functionality in clsScan, you need to add an instance of the ' TwainControlX object on the DocSF, then pass that object as the result of the function 'To use WIA only, you may leave the function declaration blank, in which case it will return Nothing 'Sample usage: ' Set TwainX = Me.Twain0.Object End Function Public Sub PreShow(ParentOpenArgs As String) 'This method gets called when the scan form supports multiple instances 'The ParentOpenArgs is generally the OpenArgs value passed to the ScanForm which in turn ' is usually the Hwnd of the form that opened the ScanForm End Sub
Image by ExposureToday from Pixabay